diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..53f8783d9994dd0361838f0bc5c56a2a8067ff21 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,23 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +GSTools/source/docs/source/pics/06_ensemble.png filter=lfs diff=lfs merge=lfs -text +GSTools/source/docs/source/pics/07_00_std.png filter=lfs diff=lfs merge=lfs -text +GSTools/source/docs/source/pics/07_03_zinnharvey.png filter=lfs diff=lfs merge=lfs -text +GSTools/source/docs/source/pics/07_04_arcsin.png filter=lfs diff=lfs merge=lfs -text +GSTools/source/docs/source/pics/09_cond_ens.png filter=lfs diff=lfs merge=lfs -text +GSTools/source/docs/source/pics/3d_gau_field.png filter=lfs diff=lfs merge=lfs -text +GSTools/source/docs/source/pics/3d_pgs.png filter=lfs diff=lfs merge=lfs -text +GSTools/source/docs/source/pics/cond_ens.png filter=lfs diff=lfs merge=lfs -text +GSTools/source/docs/source/pics/gau_field.png filter=lfs diff=lfs merge=lfs -text +GSTools/source/docs/source/pics/GS_3d_vector_field.png filter=lfs diff=lfs merge=lfs -text +GSTools/source/docs/source/pics/GS_pyvista_cut.png filter=lfs diff=lfs merge=lfs -text +GSTools/source/docs/source/pics/paraview.png filter=lfs diff=lfs merge=lfs -text +GSTools/source/docs/source/pics/pyvista_export.png filter=lfs diff=lfs merge=lfs -text +GSTools/source/docs/source/pics/srf_tut_exp_ani_rot.png filter=lfs diff=lfs merge=lfs -text +GSTools/source/docs/source/pics/srf_tut_gau_field.png filter=lfs diff=lfs merge=lfs -text +GSTools/source/docs/source/pics/srf_tut_unstr.png filter=lfs diff=lfs merge=lfs -text +GSTools/source/docs/source/pics/vario_tut_herten.png filter=lfs diff=lfs merge=lfs -text +GSTools/source/docs/source/pics/vario_tut_new_herten.png filter=lfs diff=lfs merge=lfs -text +GSTools/source/docs/source/pics/vec_srf_tut_exp.png filter=lfs diff=lfs merge=lfs -text +GSTools/source/docs/source/pics/vec_srf_tut_gau.png filter=lfs diff=lfs merge=lfs -text diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..b6410bcac6aa0b90fc2f21c985ea5f0521a6a12a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.10 + +RUN useradd -m -u 1000 user && python -m pip install --upgrade pip +USER user +ENV PATH="/home/user/.local/bin:$PATH" + +WORKDIR /app + +COPY --chown=user ./requirements.txt requirements.txt +RUN pip install --no-cache-dir --upgrade -r requirements.txt + +COPY --chown=user . /app +ENV MCP_TRANSPORT=http +ENV MCP_PORT=7860 + +EXPOSE 7860 + +CMD ["python", "GSTools/mcp_output/start_mcp.py"] diff --git a/GSTools/mcp_output/README_MCP.md b/GSTools/mcp_output/README_MCP.md new file mode 100644 index 0000000000000000000000000000000000000000..3aed65774326fb2c86afc9c48b26aabb66b6fd49 --- /dev/null +++ b/GSTools/mcp_output/README_MCP.md @@ -0,0 +1,65 @@ +# GSTools: Model Context Protocol (MCP) Service + +## Project Introduction + +GSTools is a comprehensive geostatistical Python library designed for spatial data analysis and random field modeling. It provides tools for generating spatial random fields, performing kriging interpolation, estimating variograms, and conducting advanced geostatistical analyses. The library supports multiple computational backends and offers extensive export capabilities for integration with visualization tools like ParaView and PyVista. + +## Installation Method + +To install GSTools, ensure you have Python installed on your system. GSTools requires the following dependencies: + +- Required: `numpy`, `scipy`, `matplotlib` +- Optional: `pyvista`, `cartopy` + +You can install GSTools using pip: + +``` +pip install gstools +``` + +## Quick Start + +Here's a quick example to get you started with GSTools: + +1. **Model Definition**: Create a covariance model using classes like `Gaussian`, `Exponential`, or `Matern`. +2. **Field Generation**: Initialize the `SRF` class with the model and call it with spatial coordinates. +3. **Kriging Interpolation**: Use the `Krige` class for interpolation with known data points. +4. **Export**: Export results using `vtk_export` or `to_pyvista` methods for visualization. + +Example usage: + +``` +from gstools import SRF, Gaussian + +# Define a Gaussian covariance model +model = Gaussian(dim=2, var=1, len_scale=10) + +# Generate a spatial random field +srf = SRF(model) +field = srf((100, 100)) + +# Export the field for visualization +srf.to_pyvista() +``` + +## Available Tools and Endpoints List + +- **CovModel**: Base classes and functions for covariance models, including fitting and correlation calculations. +- **SRF**: Handles spatial random field generation. +- **CondSRF**: Manages conditioned field generation. +- **Krige**: Implements kriging interpolation methods for spatial data. +- **vario_estimate**: Provides tools for empirical variogram estimation and analysis. +- **vtk_export**: Facilitates export of data for visualization in tools like ParaView and PyVista. + +## Common Issues and Notes + +- **Dependencies**: Ensure all required dependencies are installed. Optional dependencies enhance functionality but are not mandatory. +- **Environment**: GSTools is compatible with Python environments that support scientific computing libraries. +- **Performance**: The library supports backend selection for performance optimization. Use the `config` module to switch between backends like GSTools-Cython and GSTools-Core. + +## Reference Links or Documentation + +- [GSTools GitHub Repository](https://github.com/GeoStat-Framework/GSTools) +- [GSTools Documentation](https://geostat-framework.readthedocs.io/projects/gstools/en/latest/) + +For more detailed information on usage and advanced features, please refer to the official documentation and GitHub repository. \ No newline at end of file diff --git a/GSTools/mcp_output/analysis.json b/GSTools/mcp_output/analysis.json new file mode 100644 index 0000000000000000000000000000000000000000..6500f088d34e330833a66f76598eb675f18798f2 --- /dev/null +++ b/GSTools/mcp_output/analysis.json @@ -0,0 +1,541 @@ +{ + "summary": { + "repository_url": "https://github.com/GeoStat-Framework/GSTools", + "summary": "Imported via zip fallback, file count: 139", + "file_tree": { + ".git_archival.txt": { + "size": 144 + }, + ".github/workflows/main.yml": { + "size": 3938 + }, + ".readthedocs.yml": { + "size": 220 + }, + ".zenodo.json": { + "size": 1450 + }, + "AUTHORS.md": { + "size": 663 + }, + "CHANGELOG.md": { + "size": 34781 + }, + "CONTRIBUTING.md": { + "size": 1444 + }, + "README.md": { + "size": 17315 + }, + "docs/source/conf.py": { + "size": 11196 + }, + "examples/00_misc/00_tpl_stable.py": { + "size": 1974 + }, + "examples/00_misc/01_export.py": { + "size": 669 + }, + "examples/00_misc/02_check_rand_meth_sampling.py": { + "size": 2130 + }, + "examples/00_misc/04_herten.py": { + "size": 11160 + }, + "examples/00_misc/05_standalone_field.py": { + "size": 839 + }, + "examples/00_misc/grid_dim_origin_spacing.txt": { + "size": 150 + }, + "examples/01_random_field/00_gaussian.py": { + "size": 1373 + }, + "examples/01_random_field/01_srf_ensemble.py": { + "size": 1662 + }, + "examples/01_random_field/02_fancier.py": { + "size": 949 + }, + "examples/01_random_field/03_unstr_srf_export.py": { + "size": 1118 + }, + "examples/01_random_field/04_srf_merge.py": { + "size": 1648 + }, + "examples/01_random_field/05_mesh_ensemble.py": { + "size": 3278 + }, + "examples/01_random_field/06_pyvista_support.py": { + "size": 2171 + }, + "examples/01_random_field/07_higher_dimensions.py": { + "size": 2498 + }, + "examples/01_random_field/08_fourier.py": { + "size": 1528 + }, + "examples/01_random_field/09_fourier_trans.py": { + "size": 1814 + }, + "examples/02_cov_model/00_intro.py": { + "size": 2336 + }, + "examples/02_cov_model/01_basic_methods.py": { + "size": 1219 + }, + "examples/02_cov_model/02_aniso_rotation.py": { + "size": 1992 + }, + "examples/02_cov_model/03_spectral_methods.py": { + "size": 1596 + }, + "examples/02_cov_model/04_different_scales.py": { + "size": 2152 + }, + "examples/02_cov_model/05_additional_para.py": { + "size": 1436 + }, + "examples/02_cov_model/06_fitting_para_ranges.py": { + "size": 2277 + }, + "examples/03_variogram/00_fit_variogram.py": { + "size": 1067 + }, + "examples/03_variogram/01_find_best_model.py": { + "size": 2042 + }, + "examples/03_variogram/02_multi_vario.py": { + "size": 1558 + }, + "examples/03_variogram/03_directional_2d.py": { + "size": 2170 + }, + "examples/03_variogram/04_directional_3d.py": { + "size": 3300 + }, + "examples/03_variogram/05_auto_fit_variogram.py": { + "size": 1223 + }, + "examples/03_variogram/06_auto_bin_latlon.py": { + "size": 3015 + }, + "examples/04_vector_field/00_2d_vector_field.py": { + "size": 1582 + }, + "examples/04_vector_field/01_3d_vector_field.py": { + "size": 2051 + }, + "examples/05_kriging/00_simple_kriging.py": { + "size": 1396 + }, + "examples/05_kriging/01_ordinary_kriging.py": { + "size": 1506 + }, + "examples/05_kriging/02_pykrige_interface.py": { + "size": 2124 + }, + "examples/05_kriging/03_compare_kriging.py": { + "size": 1048 + }, + "examples/05_kriging/04_extdrift_kriging.py": { + "size": 696 + }, + "examples/05_kriging/05_universal_kriging.py": { + "size": 1342 + }, + "examples/05_kriging/06_detrended_kriging.py": { + "size": 852 + }, + "examples/05_kriging/07_detrended_ordinary_kriging.py": { + "size": 875 + }, + "examples/05_kriging/08_measurement_errors.py": { + "size": 1634 + }, + "examples/05_kriging/09_pseudo_inverse.py": { + "size": 1208 + }, + "examples/06_conditioned_fields/00_condition_ensemble.py": { + "size": 2297 + }, + "examples/06_conditioned_fields/01_2D_condition_ensemble.py": { + "size": 2570 + }, + "examples/07_transformations/00_log_normal.py": { + "size": 430 + }, + "examples/07_transformations/01_binary.py": { + "size": 491 + }, + "examples/07_transformations/02_discrete.py": { + "size": 1427 + }, + "examples/07_transformations/03_zinn_harvey.py": { + "size": 705 + }, + "examples/07_transformations/04_bimodal.py": { + "size": 696 + }, + "examples/07_transformations/05_combinations.py": { + "size": 1530 + }, + "examples/08_geo_coordinates/00_field_generation.py": { + "size": 2184 + }, + "examples/08_geo_coordinates/01_dwd_krige.py": { + "size": 6796 + }, + "examples/08_geo_coordinates/de_borders.txt": { + "size": 24600 + }, + "examples/08_geo_coordinates/temp_obs.txt": { + "size": 49321 + }, + "examples/09_spatio_temporal/01_precip_1d.py": { + "size": 4651 + }, + "examples/09_spatio_temporal/02_precip_2d.py": { + "size": 2083 + }, + "examples/09_spatio_temporal/03_geographic_coordinates.py": { + "size": 1111 + }, + "examples/10_normalizer/00_lognormal_kriging.py": { + "size": 1984 + }, + "examples/10_normalizer/01_auto_fit.py": { + "size": 3689 + }, + "examples/10_normalizer/02_compare.py": { + "size": 2204 + }, + "examples/11_plurigaussian/00_simple.py": { + "size": 2804 + }, + "examples/11_plurigaussian/01_pgs.py": { + "size": 3698 + }, + "examples/11_plurigaussian/02_spatial_relations.py": { + "size": 3906 + }, + "examples/11_plurigaussian/03_correlations.py": { + "size": 2325 + }, + "examples/11_plurigaussian/04_3d_pgs.py": { + "size": 2657 + }, + "examples/11_plurigaussian/05_conditioned.py": { + "size": 5355 + }, + "examples/11_plurigaussian/06_periodic.py": { + "size": 2874 + }, + "examples/12_sum_model/00_simple_sum_model.py": { + "size": 2311 + }, + "examples/12_sum_model/01_fitting_sum_model.py": { + "size": 3287 + }, + "pyproject.toml": { + "size": 3193 + }, + "src/gstools/__init__.py": { + "size": 4253 + }, + "src/gstools/config.py": { + "size": 316 + }, + "src/gstools/covmodel/__init__.py": { + "size": 1515 + }, + "src/gstools/covmodel/base.py": { + "size": 62663 + }, + "src/gstools/covmodel/fit.py": { + "size": 29723 + }, + "src/gstools/covmodel/models.py": { + "size": 30714 + }, + "src/gstools/covmodel/plot.py": { + "size": 9701 + }, + "src/gstools/covmodel/sum_tools.py": { + "size": 8304 + }, + "src/gstools/covmodel/tools.py": { + "size": 19134 + }, + "src/gstools/covmodel/tpl_models.py": { + "size": 18601 + }, + "src/gstools/field/__init__.py": { + "size": 643 + }, + "src/gstools/field/base.py": { + "size": 23802 + }, + "src/gstools/field/cond_srf.py": { + "size": 10315 + }, + "src/gstools/field/generator.py": { + "size": 29699 + }, + "src/gstools/field/pgs.py": { + "size": 5758 + }, + "src/gstools/field/plot.py": { + "size": 13182 + }, + "src/gstools/field/srf.py": { + "size": 7999 + }, + "src/gstools/field/tools.py": { + "size": 9217 + }, + "src/gstools/field/upscaling.py": { + "size": 2850 + }, + "src/gstools/krige/__init__.py": { + "size": 441 + }, + "src/gstools/krige/base.py": { + "size": 30424 + }, + "src/gstools/krige/methods.py": { + "size": 21018 + }, + "src/gstools/krige/tools.py": { + "size": 2554 + }, + "src/gstools/normalizer/__init__.py": { + "size": 903 + }, + "src/gstools/normalizer/base.py": { + "size": 8573 + }, + "src/gstools/normalizer/methods.py": { + "size": 10331 + }, + "src/gstools/normalizer/tools.py": { + "size": 6386 + }, + "src/gstools/random/__init__.py": { + "size": 483 + }, + "src/gstools/random/rng.py": { + "size": 7760 + }, + "src/gstools/random/tools.py": { + "size": 4912 + }, + "src/gstools/tools/__init__.py": { + "size": 2709 + }, + "src/gstools/tools/export.py": { + "size": 7839 + }, + "src/gstools/tools/geometric.py": { + "size": 22069 + }, + "src/gstools/tools/misc.py": { + "size": 4261 + }, + "src/gstools/tools/special.py": { + "size": 7422 + }, + "src/gstools/transform/__init__.py": { + "size": 1549 + }, + "src/gstools/transform/array.py": { + "size": 11201 + }, + "src/gstools/transform/field.py": { + "size": 20373 + }, + "src/gstools/variogram/__init__.py": { + "size": 667 + }, + "src/gstools/variogram/binning.py": { + "size": 3494 + }, + "src/gstools/variogram/variogram.py": { + "size": 20991 + }, + "tests/data/variogram.txt": { + "size": 5000 + }, + "tests/test_condition.py": { + "size": 6012 + }, + "tests/test_covmodel.py": { + "size": 16534 + }, + "tests/test_export.py": { + "size": 1974 + }, + "tests/test_field.py": { + "size": 4053 + }, + "tests/test_fouriergen.py": { + "size": 3566 + }, + "tests/test_incomprrandmeth.py": { + "size": 2110 + }, + "tests/test_krige.py": { + "size": 11094 + }, + "tests/test_latlon.py": { + "size": 5935 + }, + "tests/test_normalizer.py": { + "size": 7142 + }, + "tests/test_pgs.py": { + "size": 7161 + }, + "tests/test_randmeth.py": { + "size": 2744 + }, + "tests/test_rng.py": { + "size": 4257 + }, + "tests/test_srf.py": { + "size": 12933 + }, + "tests/test_sum_model.py": { + "size": 8835 + }, + "tests/test_temporal.py": { + "size": 2304 + }, + "tests/test_transform.py": { + "size": 6352 + }, + "tests/test_variogram_structured.py": { + "size": 11183 + }, + "tests/test_variogram_unstructured.py": { + "size": 15024 + } + }, + "processed_by": "zip_fallback", + "success": true + }, + "structure": { + "packages": [ + "source.src.gstools" + ] + }, + "dependencies": { + "has_environment_yml": false, + "has_requirements_txt": false, + "pyproject": true, + "setup_cfg": false, + "setup_py": false + }, + "entry_points": { + "imports": [], + "cli": [], + "modules": [] + }, + "llm_analysis": { + "core_modules": [ + { + "package": "src.gstools.covmodel", + "module": "base", + "functions": [ + "fit_variogram", + "cor" + ], + "classes": [ + "CovModel" + ], + "description": "Provides base classes and functions for covariance models, including fitting and correlation calculations." + }, + { + "package": "src.gstools.field", + "module": "base", + "functions": [ + "generate_field" + ], + "classes": [ + "SRF", + "CondSRF" + ], + "description": "Handles spatial random field generation and conditioned field generation." + }, + { + "package": "src.gstools.krige", + "module": "base", + "functions": [ + "kriging_interpolation" + ], + "classes": [ + "Krige" + ], + "description": "Implements kriging interpolation methods for spatial data." + }, + { + "package": "src.gstools.variogram", + "module": "variogram", + "functions": [ + "vario_estimate" + ], + "classes": [], + "description": "Provides tools for empirical variogram estimation and analysis." + }, + { + "package": "src.gstools.tools", + "module": "export", + "functions": [ + "vtk_export", + "to_pyvista" + ], + "classes": [], + "description": "Facilitates export of data for visualization in tools like ParaView and PyVista." + } + ], + "cli_commands": [], + "import_strategy": { + "primary": "import", + "fallback": "blackbox", + "confidence": 0.9 + }, + "dependencies": { + "required": [ + "numpy", + "scipy", + "matplotlib" + ], + "optional": [ + "pyvista", + "cartopy" + ] + }, + "risk_assessment": { + "import_feasibility": 0.9, + "intrusiveness_risk": "low", + "complexity": "medium" + } + }, + "deepwiki_analysis": { + "repo_url": "https://github.com/GeoStat-Framework/GSTools", + "repo_name": "GSTools", + "content": "GeoStat-Framework/GSTools\nGSTools Overview\nInstallation and Setup\nLibrary Structure and API\nCore Concepts\nCovariance Models\nSpatial Random Fields\nVariogram Theory and Estimation\nField Generation\nBasic Field Generation\nConditioned Fields\nPlurigaussian Simulation\nVariogram Analysis\nEmpirical Variogram Estimation\nModel Fitting and Validation\nDirectional and Anisotropic Analysis\nKriging and Interpolation\nKriging Methods\nGeographic and Temporal Kriging\nAdvanced Features\nField Transformations\nSum Models and Multi-Scale Analysis\nGeographic Coordinate Systems\nVisualization and Export\nField Plotting and Visualization\nData Export and Integration\nDevelopment and Testing\nCI/CD Pipeline\nDocumentation System\nGSTools Overview\ndocs/Makefile\ndocs/source/_templates/layout.html\ndocs/source/authors.rst\ndocs/source/changelog.rst\ndocs/source/conf.py\ndocs/source/contents.rst\ndocs/source/index.rst\nsrc/gstools/__init__.py\nsrc/gstools/tools/__init__.py\nPurpose and Scope\nThis document provides a comprehensive overview of GSTools, a Python library for geostatistical modeling and spatial analysis. It covers the library's core architecture, main components, and how different subsystems interact to enable spatial random field generation, kriging, and variogram analysis.\nFor specific installation procedures and backend configuration, seeInstallation and Setup. For detailed library structure and API organization, seeLibrary Structure and API.\nWhat is GSTools?\nGSTools is a comprehensive geostatistical Python library designed for spatial data analysis and random field modeling. The library provides tools for generating spatial random fields, performing kriging interpolation, estimating variograms, and conducting advanced geostatistical analyses.\nThe library supports multiple computational backends and provides extensive export capabilities for integration with visualization tools like ParaView and PyVista. GSTools is built around a modular architecture that separates core mathematical models from field generation algorithms and interpolation methods.\nSources:README.md29-44docs/source/index.rst22-37src/gstools/__init__.py1-9\nCore System Architecture\nComputational BackendsInfrastructureAdvanced FeaturesCore Geostatistical ComponentsCovModel FrameworkBase class + implementationsGaussian, Exponential, MaternSRF - Spatial Random FieldsField generation enginesRandMeth, Fourier generatorsKriging SystemSimple, Ordinary, UniversalInterpolation methodsVariogram Estimationvario_estimate functionsEmpirical variogram calculationCondSRFConditioned field generationKriging-based conditioningPGS - Plurigaussian SimulationCategorical field generationMulti-Gaussian approachSumModelMulti-scale model combinationNested covariance structuresField TransformationsData normalizationDistribution conversionsconfig moduleBackend selectionThreading controltools subpackageVTK export, geometryUtility functionsrandom subpackageRNG managementSeeded generationGSTools-CythonOpenMP parallel backendGSTools-CoreRust backend (optional)\nComputational Backends\nInfrastructure\nAdvanced Features\nCore Geostatistical Components\nCovModel FrameworkBase class + implementationsGaussian, Exponential, Matern\nSRF - Spatial Random FieldsField generation enginesRandMeth, Fourier generators\nKriging SystemSimple, Ordinary, UniversalInterpolation methods\nVariogram Estimationvario_estimate functionsEmpirical variogram calculation\nCondSRFConditioned field generationKriging-based conditioning\nPGS - Plurigaussian SimulationCategorical field generationMulti-Gaussian approach\nSumModelMulti-scale model combinationNested covariance structures\nField TransformationsData normalizationDistribution conversions\nconfig moduleBackend selectionThreading control\ntools subpackageVTK export, geometryUtility functions\nrandom subpackageRNG managementSeeded generation\nGSTools-CythonOpenMP parallel backend\nGSTools-CoreRust backend (optional)\nGSTools Core System Architecture\nThis diagram shows the fundamental architecture of GSTools, organized around four core geostatistical components that work cyclically together. TheCovModelframework provides mathematical foundations,SRFgenerates spatial fields,Krigeperforms interpolation, and variogram estimation enables model parameter fitting. Advanced features extend these capabilities while a configurable infrastructure layer manages backends and utilities.\nSources:README.md33-44src/gstools/__init__.py12-26src/gstools/__init__.py137-147\nMain Components and Code Organization\nKey Public Classesgstools Package StructureSupport SubpackagesCore Subpackagesgstools.init.pyTop-level importsPublic API definitiongstools.covmodelCovModel base classGaussian, Exponential, MaternStable, Rational, etc.gstools.fieldSRF, CondSRF, PGS classesField generation algorithmsgstools.krigeKrige base classSimple, Ordinary, Universalgstools.variogramvario_estimate functionsEmpirical variogram toolsgstools.toolsvtk_export functionsGeometric utilitiesEARTH_RADIUS constantsgstools.randomRNG managementMasterRNG classgstools.transformField transformationsData preprocessinggstools.normalizerData normalizationDistribution fittinggstools.configBackend configurationThreading settingsCovModelUser-defined modelscor() method overrideSRFSpatial Random Fieldcallmethod for generationKrigeKriging interpolationcallmethod for predictionCondSRFConditioned fieldsset_pos() method\nKey Public Classes\ngstools Package Structure\nSupport Subpackages\nCore Subpackages\ngstools.init.pyTop-level importsPublic API definition\ngstools.covmodelCovModel base classGaussian, Exponential, MaternStable, Rational, etc.\ngstools.fieldSRF, CondSRF, PGS classesField generation algorithms\ngstools.krigeKrige base classSimple, Ordinary, Universal\ngstools.variogramvario_estimate functionsEmpirical variogram tools\ngstools.toolsvtk_export functionsGeometric utilitiesEARTH_RADIUS constants\ngstools.randomRNG managementMasterRNG class\ngstools.transformField transformationsData preprocessing\ngstools.normalizerData normalizationDistribution fitting\ngstools.configBackend configurationThreading settings\nCovModelUser-defined modelscor() method override\nSRFSpatial Random Fieldcallmethod for generation\nKrigeKriging interpolationcallmethod for prediction\nCondSRFConditioned fieldsset_pos() method\nGSTools Package Structure and Code Organization\nThis diagram maps the library's package structure to its main public classes, showing how the modular organization enables different aspects of geostatistical analysis. Each subpackage contains specific functionality, with the main__init__.pyfile providing a unified public API that imports key classes and functions from their respective subpackages.\n__init__.py\nSources:src/gstools/__init__.py137-147src/gstools/__init__.py201-252src/gstools/tools/__init__.py75-83\nKey Geostatistical Workflows\nGSTools implements several fundamental geostatistical workflows through its core components:\nField Generation Workflow\nModel Definition: Create covariance model using classes likeGaussian,Exponential, orMatern\nExponential\nField Generation: InitializeSRFclass with the model and call with spatial coordinates\nConditioning(optional): UseCondSRFto honor known observations\nExport: Export results usingvtk_exportorto_pyvistamethods\nVariogram Analysis Workflow\nEmpirical Estimation: Usevario_estimatefunction to compute sample variogram\nvario_estimate\nModel Fitting: Applyfit_variogrammethod on covariance model classes\nfit_variogram\nValidation: Validate fitted parameters against empirical data\nIteration: Refine model parameters based on validation results\nKriging Interpolation Workflow\nSetup: InitializeKrigeclass with covariance model and known data points\nPrediction: Call kriging object with target locations for interpolation\nIntegration: Combine withSRFfor conditioned field generation\nSources:README.md117-227docs/source/index.rst171-293\nBackend System Architecture\nGSTools-Cython\nGSTools-Core\nGSTools supports two computational backends controlled through theconfigmodule:\nDefault Backend: UsesGSTools-Cythonwith optional OpenMP parallelization\nGSTools-Cython\nRust Backend: UsesGSTools-Corefor enhanced performance and automatic parallelization\nGSTools-Core\nRuntime Switching: Backend selection viaconfig.USE_GSTOOLS_COREflag\nconfig.USE_GSTOOLS_CORE\nThe backend system is transparent to users - the same API calls work regardless of the selected backend, with performance optimizations handled automatically.\nSources:docs/source/index.rst81-135README.md58-74\nIntegration and Export Capabilities\nGSTools provides extensive integration capabilities with the scientific Python ecosystem:\nVisualization Integration\nVTK Export:vtk_exportfunctions for ParaView visualization\nPyVista Integration:to_pyvistamethods for interactive 3D plotting\nMatplotlib Support: Built-in plotting methods for 2D visualization\nGeographic Support\nCoordinate Systems: Native support for latitude/longitude coordinates\nEarth Scaling: Built-in constants likeEARTH_RADIUSandKM_SCALE\nEARTH_RADIUS\nCartopy Integration: Seamless integration with geographic plotting libraries\nData Format Support\nNumPy Arrays: Direct compatibility with NumPy array structures\nMesh Formats: Support for structured and unstructured meshes\nMultiple Dimensions: 1D, 2D, and 3D field generation and analysis\nSources:src/gstools/tools/__init__.py112-123README.md143-184docs/source/index.rst202-246\nRefresh this wiki\nOn this page\nGSTools Overview\nPurpose and Scope\nWhat is GSTools?\nCore System Architecture\nMain Components and Code Organization\nKey Geostatistical Workflows\nField Generation Workflow\nVariogram Analysis Workflow\nKriging Interpolation Workflow\nBackend System Architecture\nIntegration and Export Capabilities\nVisualization Integration\nGeographic Support\nData Format Support", + "model": "gpt-4o-2024-08-06", + "source": "selenium", + "success": true + }, + "deepwiki_options": { + "enabled": true, + "model": "gpt-4o-2024-08-06" + }, + "risk": { + "import_feasibility": 0.9, + "intrusiveness_risk": "low", + "complexity": "medium" + } +} \ No newline at end of file diff --git a/GSTools/mcp_output/diff_report.md b/GSTools/mcp_output/diff_report.md new file mode 100644 index 0000000000000000000000000000000000000000..56cb68d04655df6a646cf4bfcaf94e215860d707 --- /dev/null +++ b/GSTools/mcp_output/diff_report.md @@ -0,0 +1,64 @@ +# GSTools Project Difference Report + +**Date:** February 2, 2026 +**Time:** 11:49:10 +**Repository:** GSTools +**Project Type:** Python Library +**Main Features:** Basic Functionality +**Intrusiveness:** None +**Workflow Status:** Success +**Test Status:** Failed + +## Project Overview + +GSTools is a Python library designed to provide basic functionalities for geostatistical modeling and analysis. The project aims to offer a robust set of tools for users in the geosciences field, facilitating the creation and manipulation of spatial data models. + +## Difference Analysis + +### New Files + +In this update, 8 new files have been added to the repository. These files likely introduce new features or enhancements to the existing functionalities of the GSTools library. However, no existing files were modified, indicating that the new additions are supplementary rather than replacements or alterations of current code. + +### Modified Files + +There were no modifications to existing files in this update. This suggests that the new features or functionalities were implemented in a way that does not interfere with the current codebase, maintaining the integrity of the existing system. + +## Technical Analysis + +### Workflow Status + +The workflow status is marked as successful, indicating that the integration and deployment processes were executed without any issues. This suggests that the new files were correctly integrated into the project structure. + +### Test Status + +The test status is marked as failed, which is a critical issue that needs immediate attention. The failure in testing indicates that the new additions may have introduced bugs or that the new functionalities are not performing as expected. This could also imply that the test cases themselves need to be updated to accommodate the new features. + +## Recommendations and Improvements + +1. **Immediate Testing Review:** Conduct a thorough review of the test cases to identify the cause of the failure. Ensure that all new functionalities are covered by appropriate test cases. + +2. **Debugging and Issue Resolution:** Identify and resolve any bugs introduced by the new files. This may involve debugging the new code and ensuring compatibility with existing functionalities. + +3. **Documentation Update:** Update the project documentation to reflect the new features and provide guidance on their usage. This will help users understand and utilize the new functionalities effectively. + +4. **Code Review:** Conduct a code review to ensure that the new additions adhere to the project's coding standards and best practices. + +## Deployment Information + +The deployment of the new files was successful, as indicated by the workflow status. However, due to the failed test status, it is recommended to hold off on any further deployment until the issues are resolved and all tests pass successfully. + +## Future Planning + +1. **Enhanced Testing Framework:** Consider enhancing the testing framework to better accommodate future updates and ensure comprehensive coverage of all functionalities. + +2. **Feature Expansion:** Plan for future expansions of the library by identifying potential areas for new features or improvements based on user feedback and industry trends. + +3. **Community Engagement:** Engage with the user community to gather feedback on the new features and identify any additional needs or issues that may not have been addressed. + +## Conclusion + +The recent update to the GSTools project introduced 8 new files, expanding the library's capabilities. While the integration was successful, the failure in testing highlights the need for immediate attention to ensure the reliability and functionality of the new additions. By addressing the test failures and updating documentation, the project can continue to provide valuable tools for geostatistical analysis. + +--- + +This report provides a comprehensive overview of the recent changes to the GSTools project, along with actionable recommendations to address current issues and plan for future improvements. \ No newline at end of file diff --git a/GSTools/mcp_output/mcp_plugin/__init__.py b/GSTools/mcp_output/mcp_plugin/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/GSTools/mcp_output/mcp_plugin/adapter.py b/GSTools/mcp_output/mcp_plugin/adapter.py new file mode 100644 index 0000000000000000000000000000000000000000..5848be3935a095fd1416c591557913a09b1ae310 --- /dev/null +++ b/GSTools/mcp_output/mcp_plugin/adapter.py @@ -0,0 +1,248 @@ +import os +import sys + +# Path settings +source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source") +sys.path.insert(0, source_path) + +# Import statements +try: + from src.gstools.covmodel import CovModel + from src.gstools.field import SRF, CondSRF, PGS + from src.gstools.krige import Krige + from src.gstools.variogram import vario_estimate + from src.gstools.tools import vtk_export, to_pyvista + from src.gstools.random import MasterRNG + from src.gstools.transform import FieldTransform + from src.gstools.normalizer import DataNormalizer + from src.gstools.config import config +except ImportError as e: + print(f"Import failed: {e}. Ensure the source directory is correctly set.") + # Fallback mode + fallback_mode = True +else: + fallback_mode = False + +class Adapter: + """ + Adapter class for GSTools library, providing methods to utilize core functionalities. + """ + + def __init__(self): + self.mode = "import" if not fallback_mode else "fallback" + + # CovModel Methods + def create_cov_model(self, model_type, **kwargs): + """ + Create a covariance model instance. + + Parameters: + - model_type: str, type of the covariance model (e.g., 'Gaussian', 'Exponential') + - kwargs: additional parameters for the model + + Returns: + - dict: status and model instance or error message + """ + try: + model = CovModel(model_type, **kwargs) + return {"status": "success", "model": model} + except Exception as e: + return {"status": "error", "message": str(e)} + + # SRF Methods + def create_srf(self, cov_model, **kwargs): + """ + Create a spatial random field instance. + + Parameters: + - cov_model: CovModel instance + - kwargs: additional parameters for the SRF + + Returns: + - dict: status and SRF instance or error message + """ + try: + srf = SRF(cov_model, **kwargs) + return {"status": "success", "srf": srf} + except Exception as e: + return {"status": "error", "message": str(e)} + + # CondSRF Methods + def create_cond_srf(self, cov_model, **kwargs): + """ + Create a conditioned spatial random field instance. + + Parameters: + - cov_model: CovModel instance + - kwargs: additional parameters for the CondSRF + + Returns: + - dict: status and CondSRF instance or error message + """ + try: + cond_srf = CondSRF(cov_model, **kwargs) + return {"status": "success", "cond_srf": cond_srf} + except Exception as e: + return {"status": "error", "message": str(e)} + + # PGS Methods + def create_pgs(self, cov_model, **kwargs): + """ + Create a plurigaussian simulation instance. + + Parameters: + - cov_model: CovModel instance + - kwargs: additional parameters for the PGS + + Returns: + - dict: status and PGS instance or error message + """ + try: + pgs = PGS(cov_model, **kwargs) + return {"status": "success", "pgs": pgs} + except Exception as e: + return {"status": "error", "message": str(e)} + + # Krige Methods + def create_krige(self, cov_model, **kwargs): + """ + Create a kriging instance. + + Parameters: + - cov_model: CovModel instance + - kwargs: additional parameters for the Krige + + Returns: + - dict: status and Krige instance or error message + """ + try: + krige = Krige(cov_model, **kwargs) + return {"status": "success", "krige": krige} + except Exception as e: + return {"status": "error", "message": str(e)} + + # Variogram Methods + def estimate_variogram(self, data, **kwargs): + """ + Estimate the variogram from data. + + Parameters: + - data: array-like, input data for variogram estimation + - kwargs: additional parameters for the variogram estimation + + Returns: + - dict: status and variogram result or error message + """ + try: + variogram = vario_estimate(data, **kwargs) + return {"status": "success", "variogram": variogram} + except Exception as e: + return {"status": "error", "message": str(e)} + + # Tools Methods + def export_to_vtk(self, field, filename): + """ + Export field data to VTK format. + + Parameters: + - field: field data to export + - filename: str, output filename + + Returns: + - dict: status and message + """ + try: + vtk_export(field, filename) + return {"status": "success", "message": "Exported to VTK successfully."} + except Exception as e: + return {"status": "error", "message": str(e)} + + def convert_to_pyvista(self, field): + """ + Convert field data to PyVista format. + + Parameters: + - field: field data to convert + + Returns: + - dict: status and PyVista object or error message + """ + try: + pv_object = to_pyvista(field) + return {"status": "success", "pyvista": pv_object} + except Exception as e: + return {"status": "error", "message": str(e)} + + # Random Methods + def create_rng(self, seed=None): + """ + Create a random number generator instance. + + Parameters: + - seed: int, optional seed for RNG + + Returns: + - dict: status and RNG instance or error message + """ + try: + rng = MasterRNG(seed) + return {"status": "success", "rng": rng} + except Exception as e: + return {"status": "error", "message": str(e)} + + # Transform Methods + def apply_field_transform(self, field, transform_type, **kwargs): + """ + Apply a transformation to a field. + + Parameters: + - field: field data to transform + - transform_type: str, type of transformation + - kwargs: additional parameters for the transformation + + Returns: + - dict: status and transformed field or error message + """ + try: + transform = FieldTransform(transform_type, **kwargs) + transformed_field = transform.apply(field) + return {"status": "success", "transformed_field": transformed_field} + except Exception as e: + return {"status": "error", "message": str(e)} + + # Normalizer Methods + def normalize_data(self, data, method, **kwargs): + """ + Normalize data using a specified method. + + Parameters: + - data: array-like, data to normalize + - method: str, normalization method + - kwargs: additional parameters for normalization + + Returns: + - dict: status and normalized data or error message + """ + try: + normalizer = DataNormalizer(method, **kwargs) + normalized_data = normalizer.normalize(data) + return {"status": "success", "normalized_data": normalized_data} + except Exception as e: + return {"status": "error", "message": str(e)} + + # Config Methods + def configure_backend(self, use_core=False): + """ + Configure the computational backend. + + Parameters: + - use_core: bool, whether to use the GSTools-Core backend + + Returns: + - dict: status and message + """ + try: + config.USE_GSTOOLS_CORE = use_core + return {"status": "success", "message": f"Backend set to {'GSTools-Core' if use_core else 'GSTools-Cython'}."} + except Exception as e: + return {"status": "error", "message": str(e)} \ No newline at end of file diff --git a/GSTools/mcp_output/mcp_plugin/main.py b/GSTools/mcp_output/mcp_plugin/main.py new file mode 100644 index 0000000000000000000000000000000000000000..fca6ec384e22f703b287550e94cc00baaaa4c4a7 --- /dev/null +++ b/GSTools/mcp_output/mcp_plugin/main.py @@ -0,0 +1,13 @@ +""" +MCP Service Auto-Wrapper - Auto-generated +""" +from mcp_service import create_app + +def main(): + """Main entry point""" + app = create_app() + return app + +if __name__ == "__main__": + app = main() + app.run() \ No newline at end of file diff --git a/GSTools/mcp_output/mcp_plugin/mcp_service.py b/GSTools/mcp_output/mcp_plugin/mcp_service.py new file mode 100644 index 0000000000000000000000000000000000000000..ce9f6cfc5e9d76fdd37e223fee9434571fb40e3f --- /dev/null +++ b/GSTools/mcp_output/mcp_plugin/mcp_service.py @@ -0,0 +1,99 @@ +import os +import sys + +# Path settings to include the local source directory +source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source") +if source_path not in sys.path: + sys.path.insert(0, source_path) + +from fastmcp import FastMCP +from gstools.covmodel import CovModel +from gstools.field import SRF +from gstools.krige import Krige +from gstools.variogram import vario_estimate + +# Create the FastMCP service application +mcp = FastMCP("geostat_service") + +@mcp.tool(name="create_cov_model", description="Create a covariance model") +def create_cov_model(model_type: str, parameters: dict) -> dict: + """ + Create a covariance model based on the specified type and parameters. + + Parameters: + - model_type: The type of covariance model (e.g., 'Gaussian', 'Exponential'). + - parameters: A dictionary of parameters for the model. + + Returns: + - A dictionary with success status and the created model or error message. + """ + try: + model = CovModel(model_type, **parameters) + return {"success": True, "result": model} + except Exception as e: + return {"success": False, "error": str(e)} + +@mcp.tool(name="generate_random_field", description="Generate a spatial random field") +def generate_random_field(model: CovModel, grid: list) -> dict: + """ + Generate a spatial random field using the specified covariance model and grid. + + Parameters: + - model: The covariance model to use for field generation. + - grid: A list defining the spatial grid. + + Returns: + - A dictionary with success status and the generated field or error message. + """ + try: + srf = SRF(model) + field = srf(grid) + return {"success": True, "result": field} + except Exception as e: + return {"success": False, "error": str(e)} + +@mcp.tool(name="perform_kriging", description="Perform kriging interpolation") +def perform_kriging(model: CovModel, data_points: list, target_points: list) -> dict: + """ + Perform kriging interpolation using the specified covariance model and data points. + + Parameters: + - model: The covariance model to use for kriging. + - data_points: A list of known data points. + - target_points: A list of target points for interpolation. + + Returns: + - A dictionary with success status and the interpolated values or error message. + """ + try: + krige = Krige(model, data_points) + interpolated_values = krige(target_points) + return {"success": True, "result": interpolated_values} + except Exception as e: + return {"success": False, "error": str(e)} + +@mcp.tool(name="estimate_variogram", description="Estimate an empirical variogram") +def estimate_variogram(data: list) -> dict: + """ + Estimate an empirical variogram from the given data. + + Parameters: + - data: A list of spatial data points. + + Returns: + - A dictionary with success status and the estimated variogram or error message. + """ + try: + variogram = vario_estimate(data) + return {"success": True, "result": variogram} + except Exception as e: + return {"success": False, "error": str(e)} + +def create_app() -> FastMCP: + """ + Create and return the FastMCP application instance. + + Returns: + - The FastMCP instance for the service. + """ + return mcp \ No newline at end of file diff --git a/GSTools/mcp_output/requirements.txt b/GSTools/mcp_output/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..519b8e52a33c493e8ed136ec7455bce8ffe0754d --- /dev/null +++ b/GSTools/mcp_output/requirements.txt @@ -0,0 +1,12 @@ +fastmcp +fastapi +uvicorn[standard] +pydantic>=2.0.0 +gstools-cython>=1,<2 +emcee>=3.0.0 +hankel>=1.0.0 +meshio>=5.1.0 +numpy>=1.20.0 +pyevtk>=1.1.1 +scipy>=1.1.0 +matplotlib diff --git a/GSTools/mcp_output/start_mcp.py b/GSTools/mcp_output/start_mcp.py new file mode 100644 index 0000000000000000000000000000000000000000..fc7fcbd9646ad53f089fc94af8129043a703325a --- /dev/null +++ b/GSTools/mcp_output/start_mcp.py @@ -0,0 +1,30 @@ + +""" +MCP Service Startup Entry +""" +import sys +import os + +project_root = os.path.dirname(os.path.abspath(__file__)) +mcp_plugin_dir = os.path.join(project_root, "mcp_plugin") +if mcp_plugin_dir not in sys.path: + sys.path.insert(0, mcp_plugin_dir) + +from mcp_service import create_app + +def main(): + """Start FastMCP service""" + app = create_app() + # Use environment variable to configure port, default 8000 + port = int(os.environ.get("MCP_PORT", "8000")) + + # Choose transport mode based on environment variable + transport = os.environ.get("MCP_TRANSPORT", "stdio") + if transport == "http": + app.run(transport="http", host="0.0.0.0", port=port) + else: + # Default to STDIO mode + app.run() + +if __name__ == "__main__": + main() diff --git a/GSTools/mcp_output/workflow_summary.json b/GSTools/mcp_output/workflow_summary.json new file mode 100644 index 0000000000000000000000000000000000000000..891fc87c85ac8b73dc3ef677231cdbf65a9ea7b7 --- /dev/null +++ b/GSTools/mcp_output/workflow_summary.json @@ -0,0 +1,194 @@ +{ + "repository": { + "name": "GSTools", + "url": "https://github.com/GeoStat-Framework/GSTools", + "local_path": "/export/zxcpu1/shiweijie/code/ghh/Code2MCP/workspace/GSTools", + "description": "Python library", + "features": "Basic functionality", + "tech_stack": "Python", + "stars": 0, + "forks": 0, + "language": "Python", + "last_updated": "", + "complexity": "medium", + "intrusiveness_risk": "low" + }, + "execution": { + "start_time": 1770004008.1445622, + "end_time": 1770004103.8815975, + "duration": 95.73703622817993, + "status": "success", + "workflow_status": "success", + "nodes_executed": [ + "download", + "analysis", + "env", + "generate", + "run", + "review", + "finalize" + ], + "total_files_processed": 1, + "environment_type": "unknown", + "llm_calls": 0, + "deepwiki_calls": 0 + }, + "tests": { + "original_project": { + "passed": false, + "details": {}, + "test_coverage": "100%", + "execution_time": 0, + "test_files": [] + }, + "mcp_plugin": { + "passed": true, + "details": {}, + "service_health": "healthy", + "startup_time": 0, + "transport_mode": "stdio", + "fastmcp_version": "unknown", + "mcp_version": "unknown" + } + }, + "analysis": { + "structure": { + "packages": [ + "source.src.gstools" + ] + }, + "dependencies": { + "has_environment_yml": false, + "has_requirements_txt": false, + "pyproject": true, + "setup_cfg": false, + "setup_py": false + }, + "entry_points": { + "imports": [], + "cli": [], + "modules": [] + }, + "risk_assessment": { + "import_feasibility": 0.9, + "intrusiveness_risk": "low", + "complexity": "medium" + }, + "deepwiki_analysis": { + "repo_url": "https://github.com/GeoStat-Framework/GSTools", + "repo_name": "GSTools", + "content": "GeoStat-Framework/GSTools\nGSTools Overview\nInstallation and Setup\nLibrary Structure and API\nCore Concepts\nCovariance Models\nSpatial Random Fields\nVariogram Theory and Estimation\nField Generation\nBasic Field Generation\nConditioned Fields\nPlurigaussian Simulation\nVariogram Analysis\nEmpirical Variogram Estimation\nModel Fitting and Validation\nDirectional and Anisotropic Analysis\nKriging and Interpolation\nKriging Methods\nGeographic and Temporal Kriging\nAdvanced Features\nField Transformations\nSum Models and Multi-Scale Analysis\nGeographic Coordinate Systems\nVisualization and Export\nField Plotting and Visualization\nData Export and Integration\nDevelopment and Testing\nCI/CD Pipeline\nDocumentation System\nGSTools Overview\ndocs/Makefile\ndocs/source/_templates/layout.html\ndocs/source/authors.rst\ndocs/source/changelog.rst\ndocs/source/conf.py\ndocs/source/contents.rst\ndocs/source/index.rst\nsrc/gstools/__init__.py\nsrc/gstools/tools/__init__.py\nPurpose and Scope\nThis document provides a comprehensive overview of GSTools, a Python library for geostatistical modeling and spatial analysis. It covers the library's core architecture, main components, and how different subsystems interact to enable spatial random field generation, kriging, and variogram analysis.\nFor specific installation procedures and backend configuration, seeInstallation and Setup. For detailed library structure and API organization, seeLibrary Structure and API.\nWhat is GSTools?\nGSTools is a comprehensive geostatistical Python library designed for spatial data analysis and random field modeling. The library provides tools for generating spatial random fields, performing kriging interpolation, estimating variograms, and conducting advanced geostatistical analyses.\nThe library supports multiple computational backends and provides extensive export capabilities for integration with visualization tools like ParaView and PyVista. GSTools is built around a modular architecture that separates core mathematical models from field generation algorithms and interpolation methods.\nSources:README.md29-44docs/source/index.rst22-37src/gstools/__init__.py1-9\nCore System Architecture\nComputational BackendsInfrastructureAdvanced FeaturesCore Geostatistical ComponentsCovModel FrameworkBase class + implementationsGaussian, Exponential, MaternSRF - Spatial Random FieldsField generation enginesRandMeth, Fourier generatorsKriging SystemSimple, Ordinary, UniversalInterpolation methodsVariogram Estimationvario_estimate functionsEmpirical variogram calculationCondSRFConditioned field generationKriging-based conditioningPGS - Plurigaussian SimulationCategorical field generationMulti-Gaussian approachSumModelMulti-scale model combinationNested covariance structuresField TransformationsData normalizationDistribution conversionsconfig moduleBackend selectionThreading controltools subpackageVTK export, geometryUtility functionsrandom subpackageRNG managementSeeded generationGSTools-CythonOpenMP parallel backendGSTools-CoreRust backend (optional)\nComputational Backends\nInfrastructure\nAdvanced Features\nCore Geostatistical Components\nCovModel FrameworkBase class + implementationsGaussian, Exponential, Matern\nSRF - Spatial Random FieldsField generation enginesRandMeth, Fourier generators\nKriging SystemSimple, Ordinary, UniversalInterpolation methods\nVariogram Estimationvario_estimate functionsEmpirical variogram calculation\nCondSRFConditioned field generationKriging-based conditioning\nPGS - Plurigaussian SimulationCategorical field generationMulti-Gaussian approach\nSumModelMulti-scale model combinationNested covariance structures\nField TransformationsData normalizationDistribution conversions\nconfig moduleBackend selectionThreading control\ntools subpackageVTK export, geometryUtility functions\nrandom subpackageRNG managementSeeded generation\nGSTools-CythonOpenMP parallel backend\nGSTools-CoreRust backend (optional)\nGSTools Core System Architecture\nThis diagram shows the fundamental architecture of GSTools, organized around four core geostatistical components that work cyclically together. TheCovModelframework provides mathematical foundations,SRFgenerates spatial fields,Krigeperforms interpolation, and variogram estimation enables model parameter fitting. Advanced features extend these capabilities while a configurable infrastructure layer manages backends and utilities.\nSources:README.md33-44src/gstools/__init__.py12-26src/gstools/__init__.py137-147\nMain Components and Code Organization\nKey Public Classesgstools Package StructureSupport SubpackagesCore Subpackagesgstools.init.pyTop-level importsPublic API definitiongstools.covmodelCovModel base classGaussian, Exponential, MaternStable, Rational, etc.gstools.fieldSRF, CondSRF, PGS classesField generation algorithmsgstools.krigeKrige base classSimple, Ordinary, Universalgstools.variogramvario_estimate functionsEmpirical variogram toolsgstools.toolsvtk_export functionsGeometric utilitiesEARTH_RADIUS constantsgstools.randomRNG managementMasterRNG classgstools.transformField transformationsData preprocessinggstools.normalizerData normalizationDistribution fittinggstools.configBackend configurationThreading settingsCovModelUser-defined modelscor() method overrideSRFSpatial Random Fieldcallmethod for generationKrigeKriging interpolationcallmethod for predictionCondSRFConditioned fieldsset_pos() method\nKey Public Classes\ngstools Package Structure\nSupport Subpackages\nCore Subpackages\ngstools.init.pyTop-level importsPublic API definition\ngstools.covmodelCovModel base classGaussian, Exponential, MaternStable, Rational, etc.\ngstools.fieldSRF, CondSRF, PGS classesField generation algorithms\ngstools.krigeKrige base classSimple, Ordinary, Universal\ngstools.variogramvario_estimate functionsEmpirical variogram tools\ngstools.toolsvtk_export functionsGeometric utilitiesEARTH_RADIUS constants\ngstools.randomRNG managementMasterRNG class\ngstools.transformField transformationsData preprocessing\ngstools.normalizerData normalizationDistribution fitting\ngstools.configBackend configurationThreading settings\nCovModelUser-defined modelscor() method override\nSRFSpatial Random Fieldcallmethod for generation\nKrigeKriging interpolationcallmethod for prediction\nCondSRFConditioned fieldsset_pos() method\nGSTools Package Structure and Code Organization\nThis diagram maps the library's package structure to its main public classes, showing how the modular organization enables different aspects of geostatistical analysis. Each subpackage contains specific functionality, with the main__init__.pyfile providing a unified public API that imports key classes and functions from their respective subpackages.\n__init__.py\nSources:src/gstools/__init__.py137-147src/gstools/__init__.py201-252src/gstools/tools/__init__.py75-83\nKey Geostatistical Workflows\nGSTools implements several fundamental geostatistical workflows through its core components:\nField Generation Workflow\nModel Definition: Create covariance model using classes likeGaussian,Exponential, orMatern\nExponential\nField Generation: InitializeSRFclass with the model and call with spatial coordinates\nConditioning(optional): UseCondSRFto honor known observations\nExport: Export results usingvtk_exportorto_pyvistamethods\nVariogram Analysis Workflow\nEmpirical Estimation: Usevario_estimatefunction to compute sample variogram\nvario_estimate\nModel Fitting: Applyfit_variogrammethod on covariance model classes\nfit_variogram\nValidation: Validate fitted parameters against empirical data\nIteration: Refine model parameters based on validation results\nKriging Interpolation Workflow\nSetup: InitializeKrigeclass with covariance model and known data points\nPrediction: Call kriging object with target locations for interpolation\nIntegration: Combine withSRFfor conditioned field generation\nSources:README.md117-227docs/source/index.rst171-293\nBackend System Architecture\nGSTools-Cython\nGSTools-Core\nGSTools supports two computational backends controlled through theconfigmodule:\nDefault Backend: UsesGSTools-Cythonwith optional OpenMP parallelization\nGSTools-Cython\nRust Backend: UsesGSTools-Corefor enhanced performance and automatic parallelization\nGSTools-Core\nRuntime Switching: Backend selection viaconfig.USE_GSTOOLS_COREflag\nconfig.USE_GSTOOLS_CORE\nThe backend system is transparent to users - the same API calls work regardless of the selected backend, with performance optimizations handled automatically.\nSources:docs/source/index.rst81-135README.md58-74\nIntegration and Export Capabilities\nGSTools provides extensive integration capabilities with the scientific Python ecosystem:\nVisualization Integration\nVTK Export:vtk_exportfunctions for ParaView visualization\nPyVista Integration:to_pyvistamethods for interactive 3D plotting\nMatplotlib Support: Built-in plotting methods for 2D visualization\nGeographic Support\nCoordinate Systems: Native support for latitude/longitude coordinates\nEarth Scaling: Built-in constants likeEARTH_RADIUSandKM_SCALE\nEARTH_RADIUS\nCartopy Integration: Seamless integration with geographic plotting libraries\nData Format Support\nNumPy Arrays: Direct compatibility with NumPy array structures\nMesh Formats: Support for structured and unstructured meshes\nMultiple Dimensions: 1D, 2D, and 3D field generation and analysis\nSources:src/gstools/tools/__init__.py112-123README.md143-184docs/source/index.rst202-246\nRefresh this wiki\nOn this page\nGSTools Overview\nPurpose and Scope\nWhat is GSTools?\nCore System Architecture\nMain Components and Code Organization\nKey Geostatistical Workflows\nField Generation Workflow\nVariogram Analysis Workflow\nKriging Interpolation Workflow\nBackend System Architecture\nIntegration and Export Capabilities\nVisualization Integration\nGeographic Support\nData Format Support", + "model": "gpt-4o-2024-08-06", + "source": "selenium", + "success": true + }, + "code_complexity": { + "cyclomatic_complexity": "medium", + "cognitive_complexity": "medium", + "maintainability_index": 75 + }, + "security_analysis": { + "vulnerabilities_found": 0, + "security_score": 85, + "recommendations": [] + } + }, + "plugin_generation": { + "files_created": [ + "mcp_output/start_mcp.py", + "mcp_output/mcp_plugin/__init__.py", + "mcp_output/mcp_plugin/mcp_service.py", + "mcp_output/mcp_plugin/adapter.py", + "mcp_output/mcp_plugin/main.py", + "mcp_output/requirements.txt", + "mcp_output/README_MCP.md" + ], + "main_entry": "start_mcp.py", + "requirements": [ + "fastmcp>=0.1.0", + "pydantic>=2.0.0" + ], + "readme_path": "/export/zxcpu1/shiweijie/code/ghh/Code2MCP/workspace/GSTools/mcp_output/README_MCP.md", + "adapter_mode": "import", + "total_lines_of_code": 0, + "generated_files_size": 0, + "tool_endpoints": 0, + "supported_features": [ + "Basic functionality" + ], + "generated_tools": [ + "Basic tools", + "Health check tools", + "Version info tools" + ] + }, + "code_review": {}, + "errors": [], + "warnings": [], + "recommendations": [ + "Improve test coverage by adding more unit tests", + "Ensure all dependencies are clearly defined in a requirements.txt or environment.yml file", + "Optimize large files for better performance", + "Enhance documentation for better clarity and user guidance", + "Implement continuous integration to automate testing and deployment", + "Refactor code to improve readability and maintainability", + "Consider adding more examples to demonstrate library usage", + "Evaluate and improve the performance of core functions", + "Ensure compatibility with the latest versions of dependencies", + "Conduct a security audit to identify and fix vulnerabilities" + ], + "performance_metrics": { + "memory_usage_mb": 0, + "cpu_usage_percent": 0, + "response_time_ms": 0, + "throughput_requests_per_second": 0 + }, + "deployment_info": { + "supported_platforms": [ + "Linux", + "Windows", + "macOS" + ], + "python_versions": [ + "3.8", + "3.9", + "3.10", + "3.11", + "3.12" + ], + "deployment_methods": [ + "Docker", + "pip", + "conda" + ], + "monitoring_support": true, + "logging_configuration": "structured" + }, + "execution_analysis": { + "success_factors": [ + "Comprehensive analysis and generation nodes executed successfully", + "Successful integration of MCP plugin with healthy service status" + ], + "failure_reasons": [], + "overall_assessment": "excellent", + "node_performance": { + "download_time": "Efficient download with no delays", + "analysis_time": "Completed within expected duration", + "generation_time": "Efficient generation with no errors", + "test_time": "Original project tests failed, but MCP plugin tests passed" + }, + "resource_usage": { + "memory_efficiency": "Memory usage data not available", + "cpu_efficiency": "CPU usage data not available", + "disk_usage": "Disk usage data not available" + } + }, + "technical_quality": { + "code_quality_score": 75, + "architecture_score": 80, + "performance_score": 70, + "maintainability_score": 75, + "security_score": 85, + "scalability_score": 80 + } +} \ No newline at end of file diff --git a/GSTools/source/.readthedocs.yml b/GSTools/source/.readthedocs.yml new file mode 100644 index 0000000000000000000000000000000000000000..4291bf87939040da74400a9299b143753030da35 --- /dev/null +++ b/GSTools/source/.readthedocs.yml @@ -0,0 +1,18 @@ +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +sphinx: + configuration: docs/source/conf.py + +formats: [pdf] + +python: + install: + - method: pip + path: . + extra_requirements: + - doc diff --git a/GSTools/source/.zenodo.json b/GSTools/source/.zenodo.json new file mode 100644 index 0000000000000000000000000000000000000000..ae527ddc5e254de8c5c8df580a8841cecb7e79e6 --- /dev/null +++ b/GSTools/source/.zenodo.json @@ -0,0 +1,49 @@ +{ + "license": "LGPL-3.0-or-later", + "contributors": [ + { + "type": "Other", + "name": "Bane Sullivan" + }, + { + "orcid": "0000-0002-2547-8102", + "affiliation": "Helmholtz Centre for Environmental Research - UFZ", + "type": "ResearchGroup", + "name": "Falk He\u00dfe" + }, + { + "orcid": "0000-0002-8783-6198", + "affiliation": "Hydrogeology Group, Department of Earth Science, Utrecht University, Netherlands", + "type": "ResearchGroup", + "name": "Alraune Zech" + }, + { + "orcid": "0000-0002-7798-7080", + "affiliation": "Helmholtz Centre for Environmental Research - UFZ", + "type": "Supervisor", + "name": "Sabine Attinger" + } + ], + "language": "eng", + "keywords": [ + "geostatistics", + "kriging", + "random fields", + "covariance models", + "variogram", + "Python", + "GeoStat-Framework" + ], + "creators": [ + { + "orcid": "0000-0001-9060-4008", + "affiliation": "Helmholtz Centre for Environmental Research - UFZ", + "name": "Sebastian M\u00fcller" + }, + { + "orcid": "0000-0001-9362-1372", + "affiliation": "Helmholtz Centre for Environmental Research - UFZ", + "name": "Lennart Sch\u00fcler" + } + ] +} \ No newline at end of file diff --git a/GSTools/source/AUTHORS.md b/GSTools/source/AUTHORS.md new file mode 100644 index 0000000000000000000000000000000000000000..2feeba611cab28c56c5835ee4790de495b6166ab --- /dev/null +++ b/GSTools/source/AUTHORS.md @@ -0,0 +1,17 @@ +# Authors + +GSTools is available on [GitHub](https://github.com/GeoStat-Framework/GSTools) +and was created by following people. + + +## Core developers + +- Sebastian Müller, GitHub: [@MuellerSeb](https://github.com/MuellerSeb), Email: +- Lennart Schüler, GitHub: [@LSchueler](https://github.com/LSchueler), Email: + + +## Contributors (in order of contributions) + +- Falk Heße, GitHub: [@fhesze](https://github.com/fhesze), Email: +- Bane Sullivan, GitHub: [@banesullivan](https://github.com/banesullivan) +- Tobias Glaubach, GitHub: [@TobiasGlaubach](https://github.com/TobiasGlaubach) diff --git a/GSTools/source/CHANGELOG.md b/GSTools/source/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..ba0713144bfad2d5aaac55f2a0e7f98eb683b4df --- /dev/null +++ b/GSTools/source/CHANGELOG.md @@ -0,0 +1,538 @@ +# Changelog + +All notable changes to **GSTools** will be documented in this file. + +## [Unreleased] - ? + +### Changes + +- replace pylint, black, and isort with ruff [#391](https://github.com/GeoStat-Framework/GSTools/pull/391) + +## [1.7.0] - Morphic Mint - 2025-04 + +### Enhancements + +- new feature: Plurigaussian simulations (PGS) ([#370](https://github.com/GeoStat-Framework/GSTools/pull/370)) + - they simulate distributions of categorical data, e.g. lithofacies, hydrofacies, soil types, or cementitious materials + - they naturally extend truncated Gaussian fields, which are already a part of GSTools through the field transformations +- new feature: support for Sum-Models ([#364](https://github.com/GeoStat-Framework/GSTools/pull/364)) + - added `SumModel` class + - represents sum of covariance models + - behaves just as a normal covariance model with kriging and field generation + - covariance models can be added with overloaded `+` operator: `model = m1 + m2` + - class is subscriptable to access sub-models by index: `m1 = model[0]` + - included models will get a nugget of 0 and the nugget is stored separately in the sum-model + - model variance is the sum of the sub-model variances + - model length-scale is weighted sum of sub-model len-scales, where the weights are the ratios of the sub-models variance to the sum variance (motivated by the integral scale, which satisfies this relation) + - anisotropy and rotation need to be the same for all included sub-models + - parameters of the sub-models can be accessed by name with added index suffix: `model[0].nu == model.nu_0` + - fitting: if `len_scale` is fixed, none of the `len_scale_` can be fixed since len_scale is calculated from variance ratios + - added Nugget class (empty SumModel) + - allow len scale of 0 in CovModel to enable a pure nugget model + - added `zero_var` and `model` attributes to Generator ABC to shortcut field generation for pure nugget models + +### Changes + +- outsourced cython code to a separate package [GSTools-Cython](https://github.com/GeoStat-Framework/GSTools-Cython) ([#376](https://github.com/GeoStat-Framework/GSTools/pull/376)) +- removed `var_raw` attribute from CovModel (was rarely used and only relevant for the truncated power law models) + - BREAKING CHANGE (but not to many should be affected) + - TPLCovModel now has a `intensity` attribute which calculates what `var_raw` was before +- simplified variogram fitting (`var_raw` was a bad idea in the first place) +- variogram plotting now handles a len-scale of 0 (to properly plot nugget models) +- fitting: when sill is given and var and nugget are deselected from fitting, an error is raised if given var+nugget is not equal to sill (before, they were reset under the hood in a strange way) + +### Bugfixes + +- `pnt_cnt` was not recalculated in `vario_estimate` when a mask was applied, together with a given sample size this resulted in an `IndexError` most of the times ([#378](https://github.com/GeoStat-Framework/GSTools/pull/378)) + + +## [1.6.1] - Periodic Peach - 2025-01 + +see [#375](https://github.com/GeoStat-Framework/GSTools/pull/375) + +- added wheels for Python 3.13 + +### Bugfixes +- fix readthedocs.com problems ([#375](https://github.com/GeoStat-Framework/GSTools/pull/375)) +- remove deprecated scipy dependency ([#375](https://github.com/GeoStat-Framework/GSTools/pull/375)) + + +## [1.6.0] - Periodic Peach - 2024-07 + +### Enhancements +- add a new spatial random field generator called `Fourier` ([#302](https://github.com/GeoStat-Framework/GSTools/pull/302)) + - create periodic spatial random fields with `Fourier`, by setting the period length with the keyword `periodic` + - add examples to showcase how to use the new generator + +### Bugfixes +- fix some Cython issues ([#354](https://github.com/GeoStat-Framework/GSTools/pull/354)) + - update deprecated conditional compilation for OpenMP support + - fix changing between Cython and Rust backends + - improve documentation regarding parallelization +- fix deprecated license identifier, now Zenodo releases work again ([#356](https://github.com/GeoStat-Framework/GSTools/pull/356)) + +### Changes +- require gstools-core v1.0.0 at least ([#361](https://github.com/GeoStat-Framework/GSTools/pull/361)) + + +## [1.5.2] - Nifty Neon - 2024-05 + +### Enhancements + +- added global variable `config.NUM_THREADS` to select number of threads for parallel computation ([#336](https://github.com/GeoStat-Framework/GSTools/pull/336)) +- speed up sampling with emcee by setting `vectorize=True` in `EnsembleSampler` ([#346](https://github.com/GeoStat-Framework/GSTools/pull/346)) +- prepare numpy 2 support ([#340](https://github.com/GeoStat-Framework/GSTools/pull/340)) + - at least numpy 2.0.0rc1 for building extensions (for Python 3.9 and above) + - check multiple numpy and scipy versions in CI + - fixed minimal versions for numpy + - use `np.asarray` everywhere with `np.atleast_(n)d` + - fix long/longlong integer issue in cython on windows by always using 64bit integers + +### Bugfixes +- build docs with latest sphinx version ([#340](https://github.com/GeoStat-Framework/GSTools/pull/340)) +- fixed zero division error in spectral density of Integral model ([#347](https://github.com/GeoStat-Framework/GSTools/pull/347)) +- minor pylint fixes for used-before-assignment issues ([#350](https://github.com/GeoStat-Framework/GSTools/pull/350)) + +### Changes +- require pyvista 0.40 at least ([#340](https://github.com/GeoStat-Framework/GSTools/pull/340)) +- require matplotlib 3.7 at least ([#350](https://github.com/GeoStat-Framework/GSTools/pull/350)) +- remove universal2 wheels for macos (we already provide separate intel and arm64 wheels) ([#350](https://github.com/GeoStat-Framework/GSTools/pull/350)) + + +## [1.5.1] - Nifty Neon - 2023-11 + +### Enhancements + +see [#317](https://github.com/GeoStat-Framework/GSTools/pull/317) + +- added wheels for Python 3.12 +- dropped support for Python 3.7 (EOL) +- linted Cython files with cython-lint +- use Cython 3 to build extensions + + +## [1.5.0] - Nifty Neon - 2023-06 + +### Enhancements +- added `temporal` flag to `CovModel` to explicitly specify spatio-temporal models [#308](https://github.com/GeoStat-Framework/GSTools/pull/308) + - rotation between spatial and temporal dimension will be ignored + - added `spatial_dim` to `CovModel` to explicitly set spatial dimension for spatio-temporal models + - if not using `spatial_dim`, the provided `dim` needs to include the possible temporal dimension + - `spatial_dim` is always one less than `field_dim` for spatio-temporal models + - also works with `latlon=True` to have a spatio-temporal model with geographic coordinates + - all plotting routines respect this + - the `Field` class now has a `temporal` attribute which forwards the model attribute + - automatic variogram fitting in kriging classes for `temporal=True` and `latlon=True` will raise an error +- added `geo_scale` to `CovModel` to have a more consistent way to set the units of the model length scale for geographic coordinates [#308](https://github.com/GeoStat-Framework/GSTools/pull/308) + - no need to use `rescale` for this anymore (was rather a hack) + - added `gs.KM_SCALE` which is the same as `gs.EARTH_RADIUS` for kilometer scaling + - added `gs.DEGREE_SCALE` for great circle distance in degrees + - added `gs.RADIAN_SCALE` for great circle distance in radians (default and previous behavior) + - yadrenko variogram respects this and assumes the great circle distances is given in the respective unit + - `vario_estimate` also has `geo_scale` now to control the units of the bins +- `vario_estimate` now forwards additional kwargs to `standard_bins` (`bin_no`, `max_dist`) [#308](https://github.com/GeoStat-Framework/GSTools/pull/308) +- added `low` and `high` arguments to `uniform` transformation [#310](https://github.com/GeoStat-Framework/GSTools/pull/310) + +### Changes +- `CovModel`s expect special arguments by keyword now [#308](https://github.com/GeoStat-Framework/GSTools/pull/308) +- always use f-strings internally [#283](https://github.com/GeoStat-Framework/GSTools/pull/283) +- removed `verbose` attribute from `RandMeth` classes [#309](https://github.com/GeoStat-Framework/GSTools/pull/309) +- all arguments for `RandMeth` classes key-word-only now except `model` [#309](https://github.com/GeoStat-Framework/GSTools/pull/309) +- rename "package" to "api" in doc structure [#290](https://github.com/GeoStat-Framework/GSTools/pull/290) + +### Bugfixes +- latex equations were not rendered correctly in docs [#290](https://github.com/GeoStat-Framework/GSTools/pull/290) + + +## [1.4.1] - Sassy Sapphire - 2022-11 + +### Enhancements +- new (Exponential-) Integral model added [#243](https://github.com/GeoStat-Framework/GSTools/pull/243) +- added wheels for Python 3.11 [#272](https://github.com/GeoStat-Framework/GSTools/pull/272) + +### Changes +- API documentation is polished and fully auto-generated now [#271](https://github.com/GeoStat-Framework/GSTools/pull/271) + +### Bugfixes +- fixed approximation of `Matern.spectrum` for big `nu` [#243](https://github.com/GeoStat-Framework/GSTools/pull/243) +- GSTools had wrong version when installed from git archive [#272](https://github.com/GeoStat-Framework/GSTools/pull/272) +- Field.plot: solve long-standing mpl slider bug [#273](https://github.com/GeoStat-Framework/GSTools/pull/273) + + +## [1.4.0] - Sassy Sapphire - 2022-08 + +### Enhancements +- added Youtube tutorial to documentation [#239](https://github.com/GeoStat-Framework/GSTools/pull/239) +- better support for custom generators [#250](https://github.com/GeoStat-Framework/GSTools/pull/250) [#259](https://github.com/GeoStat-Framework/GSTools/pull/259) +- add `valid_value_types` class variable to all field classes [#250](https://github.com/GeoStat-Framework/GSTools/pull/250) +- PyKrige: fix passed variogram in case of latlon models [#254](https://github.com/GeoStat-Framework/GSTools/pull/254) +- add bounds checks for optional arguments of `CovModel` when resetting by class attribute [#255](https://github.com/GeoStat-Framework/GSTools/pull/255) +- minor coverage improvements [#255](https://github.com/GeoStat-Framework/GSTools/pull/255) +- documentation: readability improvements [#257](https://github.com/GeoStat-Framework/GSTools/pull/257) + +### Changes +- drop Python 3.6 support (setuptools>60 needs py>3.7) [#241](https://github.com/GeoStat-Framework/GSTools/pull/241) +- move `setup.cfg` content to `pyproject.toml` ([PEP 621](https://peps.python.org/pep-0621/)) [#241](https://github.com/GeoStat-Framework/GSTools/pull/241) +- move to `src/` based package structure (better testing, building and structure) [#241](https://github.com/GeoStat-Framework/GSTools/pull/241) +- use [extension-helpers](https://pypi.org/project/extension-helpers/) for openmp support in `setup.py` [#241](https://github.com/GeoStat-Framework/GSTools/pull/241) +- increase minimal version of meshio to v5.1 [#241](https://github.com/GeoStat-Framework/GSTools/pull/241) + +### Bugfixes +- Pyvista v0.32 deprecation warning: use point_data instead of point_arrays [#237](https://github.com/GeoStat-Framework/GSTools/pull/237) +- remove deprecated scipy (v1.9) method pinv2 [#247](https://github.com/GeoStat-Framework/GSTools/pull/247) +- change float comparison in tests [#248](https://github.com/GeoStat-Framework/GSTools/pull/248) +- Cython: solve `-Wsometimes-uninitialized` warning [#255](https://github.com/GeoStat-Framework/GSTools/pull/255) + + +## [1.3.5] - Pure Pink - 2022-01 + +### Changes +- remove caps for dependencies [#229](https://github.com/GeoStat-Framework/GSTools/pull/229) +- build linux wheels with manylinux2014 for all versions ([CIBW v2.3.1](https://github.com/pypa/cibuildwheel/releases/tag/v2.3.1)) [#227](https://github.com/GeoStat-Framework/GSTools/pull/227) + +### Bugfixes +- `Field.mesh` was not compatible with [meshio](https://github.com/nschloe/meshio) v5.1+ [#227](https://github.com/GeoStat-Framework/GSTools/pull/227) + + +## [1.3.4] - Pure Pink - 2021-11 + +### Enhancements +- add GStools-Core as optional dependency [#215](https://github.com/GeoStat-Framework/GSTools/pull/215) +- provide wheels for Python 3.10 [#211](https://github.com/GeoStat-Framework/GSTools/pull/211) +- provide macOS wheels for Apple Silicon [#211](https://github.com/GeoStat-Framework/GSTools/pull/211) + +### Changes +- remove unnecessary `dim` argument in Cython code [#216](https://github.com/GeoStat-Framework/GSTools/issues/216) + + +## [1.3.3] - Pure Pink - 2021-08 + +### Enhancements +See: [#197](https://github.com/GeoStat-Framework/GSTools/issues/197) +- `gstools.transform`: + - add keywords `field`, `store`, `process` and `keep_mean` to all transformations to control storage and respect `normalizer` + - added `apply_function` transformation + - added `apply` as wrapper for all transformations + - added `transform` method to all `Field` (sub)classes as interface to `transform.apply` + - added checks for normal fields to work smoothly with recently added `normalizer` submodule +- `Field`: + - allow naming fields when generating and control storage with `store` keyword + - all subclasses now have the `post_process` keyword (apply mean, normalizer, trend) + - added subscription to access fields by name (`Field["field"]`) + - added `set_pos` method to set position tuple + - allow reusing present `pos` tuple + - added `pos`, `mesh_type`, `field_names`, `field_shape`, `all_fields` properties +- `CondSRF`: + - memory optimization by forwarding `pos` from underlying `krige` instance + - only recalculate kriging field if `pos` tuple changed (optimized ensemble generation) +- performance improvement by using `np.asarray` instead of `np.array` where possible +- updated examples to use new features +- added incomplete lower gamma function `inc_gamma_low` (for TPLGaussian spectral density) +- filter `nan` values from `cond_val` array in all kriging routines [#201](https://github.com/GeoStat-Framework/GSTools/issues/201) + +### Bugfixes +- `inc_gamma` was defined wrong for integer `s < 0` + + +## [1.3.2] - Pure Pink - 2021-07 + +### Bugfixes +- `vario_estimate` was altering the input field under certain circumstances [#180](https://github.com/GeoStat-Framework/GSTools/issues/180) +- `emcee` v3.1 now requires `nsteps` in `run_mcmc()` to be integer (called in `RNG.sample_ln_pdf`) [#184](https://github.com/GeoStat-Framework/GSTools/pull/184) + + +## [1.3.1] - Pure Pink - 2021-06 + +### Enhancements +- Standalone use of Field class [#166](https://github.com/GeoStat-Framework/GSTools/issues/166) +- add social badges in README [#169](https://github.com/GeoStat-Framework/GSTools/issues/169), [#170](https://github.com/GeoStat-Framework/GSTools/issues/170) + +### Bugfixes +- use `oldest-supported-numpy` to build cython extensions [#165](https://github.com/GeoStat-Framework/GSTools/pull/165) + + +## [1.3.0] - Pure Pink - 2021-04 + +### Topics + +#### Geographical Coordinates Support ([#113](https://github.com/GeoStat-Framework/GSTools/issues/113)) +- added boolean init parameter `latlon` to indicate a geographic model. When given, spatial dimension is fixed to `dim=3`, `anis` and `angles` will be ignored, since anisotropy is not well-defined on a sphere. +- add property `field_dim` to indicate the dimension of the resulting field. Will be 2 if `latlon=True` +- added yadrenko variogram, covariance and correlation method, since the geographic models are derived from standard models in 3D by plugging in the chordal distance of two points on a sphere derived from there great-circle distance `zeta`: + - `vario_yadrenko`: given by `variogram(2 * np.sin(zeta / 2))` + - `cov_yadrenko`: given by `covariance(2 * np.sin(zeta / 2))` + - `cor_yadrenko`: given by `correlation(2 * np.sin(zeta / 2))` +- added plotting routines for yadrenko methods described above +- the `isometrize` and `anisometrize` methods will convert `latlon` tuples (given in degree) to points on the unit-sphere in 3D and vice versa +- representation of geographical models don't display the `dim`, `anis` and `angles` parameters, but `latlon=True` +- `fit_variogram` will expect an estimated variogram with great-circle distances given in radians +- **Variogram estimation** + - `latlon` switch implemented in `estimate_vario` routine + - will return a variogram estimated by the great-circle distance (haversine formula) given in radians +- **Field** + - added plotting routines for latlon fields + - no vector fields possible on latlon fields + - corretly handle pos tuple for latlon fields + +#### Krige Unification ([#97](https://github.com/GeoStat-Framework/GSTools/issues/97)) +- Swiss Army Knife for kriging: The `Krige` class now provides everything in one place +- "Kriging the mean" is now possible with the switch `only_mean` in the call routine +- `Simple`/`Ordinary`/`Universal`/`ExtDrift`/`Detrended` are only shortcuts to `Krige` with limited input parameter list +- We now use the `covariance` function to build up the kriging matrix (instead of variogram) +- An `unbiased` switch was added to enable simple kriging (where the unbiased condition is not given) +- An `exact` switch was added to allow smother results, if a `nugget` is present in the model +- An `cond_err` parameter was added, where measurement error variances can be given for each conditional point +- pseudo-inverse matrix is now used to solve the kriging system (can be disabled by the new switch `pseudo_inv`), this is equal to solving the system with least-squares and prevents numerical errors +- added options `fit_normalizer` and `fit_variogram` to automatically fit normalizer and variogram to given data + +#### Directional Variograms and Auto-binning ([#87](https://github.com/GeoStat-Framework/GSTools/issues/87), [#106](https://github.com/GeoStat-Framework/GSTools/issues/106), [#131](https://github.com/GeoStat-Framework/GSTools/issues/131)) +- new routine name `vario_estimate` instead of `vario_estimate_unstructured` (old kept for legacy code) for simplicity +- new routine name `vario_estimate_axis` instead of `vario_estimate_structured` (old kept for legacy code) for simplicity +- **vario_estimate** + - added simple automatic binning routine to determine bins from given data (one third of box diameter as max bin distance, sturges rule for number of bins) + - allow to pass multiple fields for joint variogram estimation (e.g. for daily precipitation) on same mesh + - `no_data` option added to allow missing values + - **masked fields** + - user can now pass a masked array (or a list of masked arrays) to deselect data points. + - in addition, a `mask` keyword was added to provide an external mask + - **directional variograms** + - diretional variograms can now be estimated + - either provide a list of direction vectors or angles for directions (spherical coordinates) + - can be controlled by given angle tolerance and (optional) bandwidth + - prepared for nD + - structured fields (pos tuple describes axes) can now be passed to estimate an isotropic or directional variogram + - distance calculation in cython routines in now independent of dimension +- **vario_estimate_axis** + - estimation along array axis now possible in arbitrary dimensions + - `no_data` option added to allow missing values (sovles [#83](https://github.com/GeoStat-Framework/GSTools/issues/83)) + - axis can be given by name (`"x"`, `"y"`, `"z"`) or axis number (`0`, `1`, `2`, `3`, ...) + +#### Better Variogram fitting ([#78](https://github.com/GeoStat-Framework/GSTools/issues/78), [#145](https://github.com/GeoStat-Framework/GSTools/pull/145)) +- fixing sill possible now +- `loss` is now selectable for smoother handling of outliers +- r2 score can now be returned to get an impression of the goodness of fitting +- weights can be passed +- instead of deselecting parameters, one can also give fix values for each parameter +- default init guess for `len_scale` is now mean of given bin-centers +- default init guess for `var` and `nugget` is now mean of given variogram values + +#### CovModel update ([#109](https://github.com/GeoStat-Framework/GSTools/issues/109), [#122](https://github.com/GeoStat-Framework/GSTools/issues/122), [#157](https://github.com/GeoStat-Framework/GSTools/pull/157)) +- add new `rescale` argument and attribute to the `CovModel` class to be able to rescale the `len_scale` (usefull for unit conversion or rescaling `len_scale` to coincide with the `integral_scale` like it's the case with the Gaussian model) + See: [#90](https://github.com/GeoStat-Framework/GSTools/issues/90), [GeoStat-Framework/PyKrige#119](https://github.com/GeoStat-Framework/PyKrige/issues/119) +- added new `len_rescaled` attribute to the `CovModel` class, which is the rescaled `len_scale`: `len_rescaled = len_scale / rescale` +- new method `default_rescale` to provide default rescale factor (can be overridden) +- remove `doctest` calls +- docstring updates in `CovModel` and derived models +- updated all models to use the `cor` routine and make use of the `rescale` argument (See: [#90](https://github.com/GeoStat-Framework/GSTools/issues/90)) +- TPL models got a separate base class to not repeat code +- added **new models** (See: [#88](https://github.com/GeoStat-Framework/GSTools/issues/88)): + - `HyperSpherical`: (Replaces the old `Intersection` model) Derived from the intersection of hyper-spheres in arbitrary dimensions. Coincides with the linear model in 1D, the circular model in 2D and the classical spherical model in 3D + - `SuperSpherical`: like the HyperSpherical, but the shape parameter derived from dimension can be set by the user. Coincides with the HyperSpherical model by default + - `JBessel`: a hole model valid in all dimensions. The shape parameter controls the dimension it was derived from. For `nu=0.5` this model coincides with the well known `wave` hole model. + - `TPLSimple`: a simple truncated power law controlled by a shape parameter `nu`. Coincides with the truncated linear model for `nu=1` + - `Cubic`: to be compatible with scikit-gstat in the future +- all arguments are now stored as float internally ([#157](https://github.com/GeoStat-Framework/GSTools/pull/157)) +- string representation of the `CovModel` class is now using a float precision (`CovModel._prec=3`) to truncate longish output +- string representation of the `CovModel` class now only shows `anis` and `angles` if model is anisotropic resp. rotated +- dimension validity check: raise a warning, if given model is not valid in the desired dimension (See: [#86](https://github.com/GeoStat-Framework/GSTools/issues/86)) + +#### Normalizer, Trend and Mean ([#124](https://github.com/GeoStat-Framework/GSTools/issues/124)) + +- new `normalize` submodule containing power-transforms for data to gain normality +- Base-Class: `Normalizer` providing basic functionality including maximum likelihood fitting +- added: `LogNormal`, `BoxCox`, `BoxCoxShift`, `YeoJohnson`, `Modulus` and `Manly` +- normalizer, trend and mean can be passed to SRF, Krige and variogram estimation routines + - A trend can be a callable function, that represents a trend in input data. For example a linear decrease of temperature with height. + - The normalizer will be applied after the data was detrended, i.e. the trend was substracted from the data, in order to gain normality. + - The mean is now interpreted as the mean of the normalized data. The user could also provide a callable mean, but it is mostly meant to be constant. + +#### Arbitrary dimensions ([#112](https://github.com/GeoStat-Framework/GSTools/issues/112)) +- allow arbitrary dimensions in all routines (CovModel, Krige, SRF, variogram) +- anisotropy and rotation following a generalization of tait-bryan angles +- `CovModel` provides `isometrize` and `anisometrize` routines to convert points + +#### New Class for Conditioned Random Fields ([#130](https://github.com/GeoStat-Framework/GSTools/issues/130)) +- **THIS BREAKS BACKWARD COMPATIBILITY** +- `CondSRF` replaces the conditioning feature of the SRF class, which was cumbersome and limited to Ordinary and Simple kriging +- `CondSRF` behaves similar to the `SRF` class, but instead of a covariance model, it takes a kriging class as input. With this kriging class, all conditioning related settings are defined. + +### Enhancements +- Python 3.9 Support [#107](https://github.com/GeoStat-Framework/GSTools/issues/107) +- add routines to format struct. pos tuple by given `dim` or `shape` +- add routine to format struct. pos tuple by given `shape` (variogram helper) +- remove `field.tools` subpackage +- support `meshio>=4.0` and add as dependency +- PyVista mesh support [#59](https://github.com/GeoStat-Framework/GSTools/issues/59) +- added `EARTH_RADIUS` as constant providing earths radius in km (can be used to rescale models) +- add routines `latlon2pos` and `pos2latlon` to convert lat-lon coordinates to points on unit-sphere and vice versa +- a lot of new examples and tutorials +- `RandMeth` class got a switch to select the sampling strategy +- plotter for n-D fields added [#141](https://github.com/GeoStat-Framework/GSTools/issues/141) +- antialias for contour plots of 2D fields [#141](https://github.com/GeoStat-Framework/GSTools/issues/141) +- building from source is now configured with `pyproject.toml` to care about build dependencies, see [#154](https://github.com/GeoStat-Framework/GSTools/issues/154) + +### Changes +- drop support for Python 3.5 [#146](https://github.com/GeoStat-Framework/GSTools/pull/146) +- added a finit limit for shape-parameters in some `CovModel`s [#147](https://github.com/GeoStat-Framework/GSTools/pull/147) +- drop usage of `pos2xyz` and `xyz2pos` +- remove structured option from generators (structured pos need to be converted first) +- explicitly assert dim=2,3 when generating vector fields +- simplify `pre_pos` routine to save pos tuple and reformat it an unstructured tuple +- simplify field shaping +- simplify plotting routines +- only the `"unstructured"` keyword is recognized everywhere, everything else is interpreted as `"structured"` (e.g. `"rectilinear"`) +- use GitHub-Actions instead of TravisCI +- parallel build now controlled by env-var `GSTOOLS_BUILD_PARALLEL=1`, see [#154](https://github.com/GeoStat-Framework/GSTools/issues/154) +- install extra target for `[dev]` dropped, can be reproduced by `pip install gstools[test, doc]`, see [#154](https://github.com/GeoStat-Framework/GSTools/issues/154) + +### Bugfixes +- typo in keyword argument for vario_estimate_structured [#80](https://github.com/GeoStat-Framework/GSTools/issues/80) +- isotropic rotation of SRF was not possible [#100](https://github.com/GeoStat-Framework/GSTools/issues/100) +- `CovModel.opt_arg` now sorted [#103](https://github.com/GeoStat-Framework/GSTools/issues/103) +- `CovModel.fit`: check if weights are given as a string (numpy comparison error) [#111](https://github.com/GeoStat-Framework/GSTools/issues/111) +- several pylint fixes ([#159](https://github.com/GeoStat-Framework/GSTools/pull/159)) + + +## [1.2.1] - Volatile Violet - 2020-04-14 + +### Bugfixes +- `ModuleNotFoundError` is not present in py35 +- Fixing Cressie-Bug #76 +- Adding analytical formula for integral scales of rational and stable model +- remove prange from IncomprRandMeth summators to prevent errors on Win and macOS + + +## [1.2.0] - Volatile Violet - 2020-03-20 + +### Enhancements +- different variogram estimator functions can now be used #51 +- the TPLGaussian and TPLExponential now have analytical spectra #67 +- added property `is_isotropic` to `CovModel` #67 +- reworked the whole krige sub-module to provide multiple kriging methods #67 + - Simple + - Ordinary + - Universal + - External Drift Kriging + - Detrended Kriging +- a new transformation function for discrete fields has been added #70 +- reworked tutorial section in the documentation #63 +- pyvista interface #29 + +### Changes +- Python versions 2.7 and 3.4 are no longer supported #40 #43 +- `CovModel`: in 3D the input of anisotropy is now treated slightly different: #67 + - single given anisotropy value [e] is converted to [1, e] (it was [e, e] before) + - two given length-scales [l_1, l_2] are converted to [l_1, l_2, l_2] (it was [l_1, l_2, l_1] before) + +### Bugfixes +- a race condition in the structured variogram estimation has been fixed #51 + + +## [1.1.1] - Reverberating Red - 2019-11-08 + +### Enhancements +- added a changelog. See: [commit fbea883](https://github.com/GeoStat-Framework/GSTools/commit/fbea88300d0862393e52f4b7c3d2b15c2039498b) + +### Changes +- deprecation warnings are now printed if Python versions 2.7 or 3.4 are used #40 #41 + +### Bugfixes +- define spectral_density instead of spectrum in covariance models since Cov-base derives spectrum. See: [commit 00f2747](https://github.com/GeoStat-Framework/GSTools/commit/00f2747fd0503ff8806f2eebfba36acff813416b) +- better boundaries for `CovModel` parameters. See: https://github.com/GeoStat-Framework/GSTools/issues/37 + + +## [1.1.0] - Reverberating Red - 2019-10-01 + +### Enhancements +- by using Cython for all the heavy computations, we could achieve quite some speed ups and reduce the memory consumption significantly #16 +- parallel computation in Cython is now supported with the help of OpenMP and the performance increase is nearly linear with increasing cores #16 +- new submodule `krige` providing simple (known mean) and ordinary (estimated mean) kriging working analogous to the srf class +- interface to pykrige to use the gstools `CovModel` with the pykrige routines (https://github.com/bsmurphy/PyKrige/issues/124) +- the srf class now provides a `plot` and a `vtk_export` routine +- incompressible flow fields can now be generated #14 +- new submodule providing several field transformations like: Zinn&Harvey, log-normal, bimodal, ... #13 +- Python 3.4 and 3.7 wheel support #19 +- field can now be generated directly on meshes from [meshio](https://github.com/nschloe/meshio) and [ogs5py](https://github.com/GeoStat-Framework/ogs5py), see: [commit f4a3439](https://github.com/GeoStat-Framework/GSTools/commit/f4a3439400b81d8d9db81a5f7fbf6435f603cf05) +- the srf and kriging classes now store the last `pos`, `mesh_type` and `field` values to keep them accessible, see: [commit 29f7f1b](https://github.com/GeoStat-Framework/GSTools/commit/29f7f1b029866379ce881f44765f72534d757fae) +- tutorials on all important features of GSTools have been written for you guys #20 +- a new interface to pyvista is provided to export fields to python vtk representation, which can be used for plotting, exploring and exporting fields #29 + +### Changes +- the license was changed from GPL to LGPL in order to promote the use of this library #25 +- the rotation angles are now interpreted in positive direction (counter clock wise) +- the `force_moments` keyword was removed from the SRF call method, it is now in provided as a field transformation #13 +- drop support of python implementations of the variogram estimators #18 +- the `variogram_normed` method was removed from the `CovModel` class due to redundance [commit 25b1647](https://github.com/GeoStat-Framework/GSTools/commit/25b164722ac6744ebc7e03f3c0bf1c30be1eba89) +- the position vector of 1D fields does not have to be provided in a list-like object with length 1 [commit a6f5be8](https://github.com/GeoStat-Framework/GSTools/commit/a6f5be8bfd2db1f002e7889ecb8e9a037ea08886) + +### Bugfixes +- several minor bugfixes + + +## [1.0.1] - Bouncy Blue - 2019-01-18 + +### Bugfixes +- fixed Numpy and Cython version during build process + + +## [1.0.0] - Bouncy Blue - 2019-01-16 + +### Enhancements +- added a new covariance class, which allows the easy usage of arbitrary covariance models +- added many predefined covariance models, including truncated power law models +- added [tutorials](https://geostat-framework.readthedocs.io/projects/gstools/en/latest/tutorials.html) and examples, showing and explaining the main features of GSTools +- variogram models can be fitted to data +- prebuilt binaries for many Linux distributions, Mac OS and Windows, making the installation, especially of the Cython code, much easier +- the generated fields can now easily be exported to vtk files +- variance scaling is supported for coarser grids +- added pure Python versions of the variogram estimators, in case somebody has problems compiling Cython code +- the [documentation](https://geostat-framework.readthedocs.io/projects/gstools/en/latest/) is now a lot cleaner and easier to use +- the code is a lot cleaner and more consistent now +- unit tests are now automatically tested when new code is pushed +- test coverage of code is shown +- GeoStat Framework now has a website, visit us: https://geostat-framework.github.io/ + +### Changes +- release is not downwards compatible with release v0.4.0 +- SRF creation has been adapted for the `CovModel` +- a tuple `pos` is now used instead of `x`, `y`, and `z` for the axes +- renamed `estimate_unstructured` and `estimate_structured` to `vario_estimate_unstructured` and `vario_estimate_structured` for less ambiguity + +### Bugfixes +- several minor bugfixes + + +## [0.4.0] - Glorious Green - 2018-07-17 + +### Bugfixes +- import of cython functions put into a try-block + + +## [0.3.6] - Original Orange - 2018-07-17 + +First release of GSTools. + + +[Unreleased]: https://github.com/GeoStat-Framework/gstools/compare/v1.7.0...HEAD +[1.7.0]: https://github.com/GeoStat-Framework/gstools/compare/v1.6.1...v1.7.0 +[1.6.1]: https://github.com/GeoStat-Framework/gstools/compare/v1.6.0...v1.6.1 +[1.6.0]: https://github.com/GeoStat-Framework/gstools/compare/v1.5.2...v1.6.0 +[1.5.2]: https://github.com/GeoStat-Framework/gstools/compare/v1.5.1...v1.5.2 +[1.5.1]: https://github.com/GeoStat-Framework/gstools/compare/v1.5.0...v1.5.1 +[1.5.0]: https://github.com/GeoStat-Framework/gstools/compare/v1.4.1...v1.5.0 +[1.4.1]: https://github.com/GeoStat-Framework/gstools/compare/v1.4.0...v1.4.1 +[1.4.0]: https://github.com/GeoStat-Framework/gstools/compare/v1.3.5...v1.4.0 +[1.3.5]: https://github.com/GeoStat-Framework/gstools/compare/v1.3.4...v1.3.5 +[1.3.4]: https://github.com/GeoStat-Framework/gstools/compare/v1.3.3...v1.3.4 +[1.3.3]: https://github.com/GeoStat-Framework/gstools/compare/v1.3.2...v1.3.3 +[1.3.2]: https://github.com/GeoStat-Framework/gstools/compare/v1.3.1...v1.3.2 +[1.3.1]: https://github.com/GeoStat-Framework/gstools/compare/v1.3.0...v1.3.1 +[1.3.0]: https://github.com/GeoStat-Framework/gstools/compare/v1.2.1...v1.3.0 +[1.2.1]: https://github.com/GeoStat-Framework/gstools/compare/v1.2.0...v1.2.1 +[1.2.0]: https://github.com/GeoStat-Framework/gstools/compare/v1.1.1...v1.2.0 +[1.1.1]: https://github.com/GeoStat-Framework/gstools/compare/v1.1.0...v1.1.1 +[1.1.0]: https://github.com/GeoStat-Framework/gstools/compare/v1.0.1...v1.1.0 +[1.0.1]: https://github.com/GeoStat-Framework/gstools/compare/v1.0.0...v1.0.1 +[1.0.0]: https://github.com/GeoStat-Framework/gstools/compare/0.4.0...v1.0.0 +[0.4.0]: https://github.com/GeoStat-Framework/gstools/compare/0.3.6...0.4.0 +[0.3.6]: https://github.com/GeoStat-Framework/gstools/releases/tag/0.3.6 diff --git a/GSTools/source/CITATION.bib b/GSTools/source/CITATION.bib new file mode 100644 index 0000000000000000000000000000000000000000..3ea8812247801de72deea4f629aed6e0ccf55c8a --- /dev/null +++ b/GSTools/source/CITATION.bib @@ -0,0 +1,11 @@ +@Article{gmd-15-3161-2022, +AUTHOR = {M\"uller, S. and Sch\"uler, L. and Zech, A. and He{\ss}e, F.}, +TITLE = {\texttt{GSTools} v1.3: a toolbox for geostatistical modelling in Python}, +JOURNAL = {Geoscientific Model Development}, +VOLUME = {15}, +YEAR = {2022}, +NUMBER = {7}, +PAGES = {3161--3182}, +URL = {https://gmd.copernicus.org/articles/15/3161/2022/}, +DOI = {10.5194/gmd-15-3161-2022} +} diff --git a/GSTools/source/CONTRIBUTING.md b/GSTools/source/CONTRIBUTING.md new file mode 100644 index 0000000000000000000000000000000000000000..6035900450c741b88edbf76fb00247f6ff3dd77e --- /dev/null +++ b/GSTools/source/CONTRIBUTING.md @@ -0,0 +1,38 @@ +# How to Contribute to GSTools + +We are happy about all contributions! :thumbsup: + + +## Did you find a bug? + +- Ensure that the bug was not already reported under +[GitHub issues](https://github.com/GeoStat-Framework/GSTools/issues) +- If the bug wasn't already reported, open a +[new issue](https://github.com/GeoStat-Framework/GSTools/issues) with a clear +description of the problem and if possible with a +[minimal working example](https://en.wikipedia.org/wiki/Minimal_working_example). +- please add the version number to the issue: + +```python +import gstools +print(gstools.__version__) +``` + + +## Do you have suggestions for new features? + +Open a [new issue](https://github.com/GeoStat-Framework/GSTools/issues) +with your idea or suggestion and we'd love to discuss about it. + + +## Do you want to enhance GSTools or fix something? + +- Fork the repo on [GitHub](https://github.com/GeoStat-Framework/GSTools) +- Add yourself to AUTHORS.md (if you want to). +- We use [Ruff](https://github.com/psf/black) to check and format the code. + Please use the scripts `ruff check src/gstools` and + `ruff format --diff src/gstools/` after you have written your code. +- Add some tests if possible. +- Add an example showing your new feature in one of the examples sub-folders if possible. + Follow this [Sphinx-Gallary guide](https://sphinx-gallery.github.io/stable/syntax.html#embed-rst-in-your-example-python-files). +- Push to your fork and submit a pull request. diff --git a/GSTools/source/LICENSE b/GSTools/source/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..0a041280bd00a9d068f503b8ee7ce35214bd24a1 --- /dev/null +++ b/GSTools/source/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/GSTools/source/README.md b/GSTools/source/README.md new file mode 100644 index 0000000000000000000000000000000000000000..1947df0bfb698112bf525a0a14b0e9c25620e4ae --- /dev/null +++ b/GSTools/source/README.md @@ -0,0 +1,443 @@ +# Welcome to GSTools +[![GMD](https://img.shields.io/badge/GMD-10.5194%2Fgmd--15--3161--2022-orange)](https://doi.org/10.5194/gmd-15-3161-2022) +[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.1313628.svg)](https://doi.org/10.5281/zenodo.1313628) +[![PyPI version](https://badge.fury.io/py/gstools.svg)](https://badge.fury.io/py/gstools) +[![Conda Version](https://img.shields.io/conda/vn/conda-forge/gstools.svg)](https://anaconda.org/conda-forge/gstools) +[![Build Status](https://github.com/GeoStat-Framework/GSTools/workflows/Continuous%20Integration/badge.svg?branch=main)](https://github.com/GeoStat-Framework/GSTools/actions) +[![Coverage Status](https://coveralls.io/repos/github/GeoStat-Framework/GSTools/badge.svg?branch=main)](https://coveralls.io/github/GeoStat-Framework/GSTools?branch=main) +[![Documentation Status](https://readthedocs.org/projects/gstools/badge/?version=latest)](https://geostat-framework.readthedocs.io/projects/gstools/en/stable/?badge=stable) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) + +

+GSTools-LOGO +

+ +

Get in Touch!

+

+GH-Discussions +Email +

+ +

Youtube Tutorial on GSTools

+ +

+ +GSTools Transform 22 tutorial + +

+ +## Purpose + + + +GSTools provides geostatistical tools for various purposes: +- random field generation, including periodic boundaries +- simple, ordinary, universal and external drift kriging +- conditioned field generation +- incompressible random vector field generation +- (automated) variogram estimation and fitting +- directional variogram estimation and modelling +- data normalization and transformation +- many readily provided and even user-defined covariance models +- metric spatio-temporal modelling +- plurigaussian field simulations (PGS) +- plotting and exporting routines + + +## Installation + + +### conda + +GSTools can be installed via [conda][conda_link] on Linux, Mac, and Windows. +Install the package by typing the following command in a command terminal: + + conda install gstools + +In case conda forge is not set up for your system yet, see the easy to follow +instructions on [conda forge][conda_forge_link]. Using conda, the parallelized +version of GSTools should be installed. + + +### pip + +GSTools can be installed via [pip][pip_link] on Linux, Mac, and Windows. +On Windows you can install [WinPython][winpy_link] to get Python and pip +running. Install the package by typing the following command in a command terminal: + + pip install gstools + +To install the latest development version via pip, see the +[documentation][doc_install_link]. +One thing to point out is that this way, the non-parallel version of GSTools +is installed. In case you want the parallel version, follow these easy +[steps][doc_install_link]. + + +## Citation + +If you are using GSTools in your publication please cite our paper: + +> Müller, S., Schüler, L., Zech, A., and Heße, F.: +> GSTools v1.3: a toolbox for geostatistical modelling in Python, +> Geosci. Model Dev., 15, 3161–3182, https://doi.org/10.5194/gmd-15-3161-2022, 2022. + +You can cite the Zenodo code publication of GSTools by: + +> Sebastian Müller & Lennart Schüler. GeoStat-Framework/GSTools. Zenodo. https://doi.org/10.5281/zenodo.1313628 + +If you want to cite a specific version, have a look at the [Zenodo site](https://doi.org/10.5281/zenodo.1313628). + + +## Documentation for GSTools + +You can find the documentation under [geostat-framework.readthedocs.io][doc_link]. + + +### Tutorials and Examples + +The documentation also includes some [tutorials][tut_link], showing the most important use cases of GSTools, which are + +- [Random Field Generation][tut1_link] +- [The Covariance Model][tut2_link] +- [Variogram Estimation][tut3_link] +- [Random Vector Field Generation][tut4_link] +- [Kriging][tut5_link] +- [Conditioned random field generation][tut6_link] +- [Field transformations][tut7_link] +- [Geographic Coordinates][tut8_link] +- [Spatio-Temporal Modelling][tut9_link] +- [Normalizing Data][tut10_link] +- [Plurigaussian Field Generation (PGS)][tut11_link] +- [Miscellaneous examples][tut0_link] + +The associated python scripts are provided in the `examples` folder. + + +## Spatial Random Field Generation + +The core of this library is the generation of spatial random fields. These fields are generated using the randomisation method, described by [Heße et al. 2014][rand_link]. + +[rand_link]: https://doi.org/10.1016/j.envsoft.2014.01.013 + + +### Examples + +#### Gaussian Covariance Model + +This is an example of how to generate a 2 dimensional spatial random field with a gaussian covariance model. + +```python +import gstools as gs +# structured field with a size 100x100 and a grid-size of 1x1 +x = y = range(100) +model = gs.Gaussian(dim=2, var=1, len_scale=10) +srf = gs.SRF(model) +srf((x, y), mesh_type='structured') +srf.plot() +``` +

+Random field +

+ +GSTools also provides support for [geographic coordinates](https://en.wikipedia.org/wiki/Geographic_coordinate_system). +This works perfectly well with [cartopy](https://scitools.org.uk/cartopy/docs/latest/index.html). + +```python +import matplotlib.pyplot as plt +import cartopy.crs as ccrs +import gstools as gs +# define a structured field by latitude and longitude +lat = lon = range(-80, 81) +model = gs.Gaussian(latlon=True, len_scale=777, geo_scale=gs.KM_SCALE) +srf = gs.SRF(model, seed=12345) +field = srf.structured((lat, lon)) +# Orthographic plotting with cartopy +ax = plt.subplot(projection=ccrs.Orthographic(-45, 45)) +cont = ax.contourf(lon, lat, field, transform=ccrs.PlateCarree()) +ax.coastlines() +ax.set_global() +plt.colorbar(cont) +``` + +

+lat-lon random field +

+ +A similar example but for a three dimensional field is exported to a [VTK](https://vtk.org/) file, which can be visualized with [ParaView](https://www.paraview.org/) or [PyVista](https://docs.pyvista.org) in Python: + +```python +import gstools as gs +# structured field with a size 100x100x100 and a grid-size of 1x1x1 +x = y = z = range(100) +model = gs.Gaussian(dim=3, len_scale=[16, 8, 4], angles=(0.8, 0.4, 0.2)) +srf = gs.SRF(model) +srf((x, y, z), mesh_type='structured') +srf.vtk_export('3d_field') # Save to a VTK file for ParaView + +mesh = srf.to_pyvista() # Create a PyVista mesh for plotting in Python +mesh.contour(isosurfaces=8).plot() +``` + +

+3d Random field +

+ + +## Estimating and Fitting Variograms + +The spatial structure of a field can be analyzed with the variogram, which contains the same information as the covariance function. + +All covariance models can be used to fit given variogram data by a simple interface. + +### Example + +This is an example of how to estimate the variogram of a 2 dimensional unstructured field and estimate the parameters of the covariance +model again. + +```python +import numpy as np +import gstools as gs +# generate a synthetic field with an exponential model +x = np.random.RandomState(19970221).rand(1000) * 100. +y = np.random.RandomState(20011012).rand(1000) * 100. +model = gs.Exponential(dim=2, var=2, len_scale=8) +srf = gs.SRF(model, mean=0, seed=19970221) +field = srf((x, y)) +# estimate the variogram of the field +bin_center, gamma = gs.vario_estimate((x, y), field) +# fit the variogram with a stable model. (no nugget fitted) +fit_model = gs.Stable(dim=2) +fit_model.fit_variogram(bin_center, gamma, nugget=False) +# output +ax = fit_model.plot(x_max=max(bin_center)) +ax.scatter(bin_center, gamma) +print(fit_model) +``` + +Which gives: + +```python +Stable(dim=2, var=1.85, len_scale=7.42, nugget=0.0, anis=[1.0], angles=[0.0], alpha=1.09) +``` + +

+Variogram +

+ + +## Kriging and Conditioned Random Fields + +An important part of geostatistics is Kriging and conditioning spatial random +fields to measurements. With conditioned random fields, an ensemble of field realizations with their variability depending on the proximity of the measurements can be generated. + +### Example +For better visualization, we will condition a 1d field to a few "measurements", generate 100 realizations and plot them: + +```python +import numpy as np +import matplotlib.pyplot as plt +import gstools as gs + +# conditions +cond_pos = [0.3, 1.9, 1.1, 3.3, 4.7] +cond_val = [0.47, 0.56, 0.74, 1.47, 1.74] + +# conditioned spatial random field class +model = gs.Gaussian(dim=1, var=0.5, len_scale=2) +krige = gs.krige.Ordinary(model, cond_pos, cond_val) +cond_srf = gs.CondSRF(krige) +# same output positions for all ensemble members +grid_pos = np.linspace(0.0, 15.0, 151) +cond_srf.set_pos(grid_pos) + +# seeded ensemble generation +seed = gs.random.MasterRNG(20170519) +for i in range(100): + field = cond_srf(seed=seed(), store=f"field_{i}") + plt.plot(grid_pos, field, color="k", alpha=0.1) +plt.scatter(cond_pos, cond_val, color="k") +plt.show() +``` + +

+Conditioned +

+ +## User Defined Covariance Models + +One of the core-features of GSTools is the powerful +[CovModel][cov_link] +class, which allows to easy define covariance models by the user. + +### Example + +Here we re-implement the Gaussian covariance model by defining just a +[correlation][cor_link] function, which takes a non-dimensional distance ``h = r/l``: + +```python +import numpy as np +import gstools as gs +# use CovModel as the base-class +class Gau(gs.CovModel): + def cor(self, h): + return np.exp(-h**2) +``` + +And that's it! With ``Gau`` you now have a fully working covariance model, +which you could use for field generation or variogram fitting as shown above. + +Have a look at the [documentation ][doc_link] for further information on incorporating +optional parameters and optimizations. + + +## Incompressible Vector Field Generation + +Using the original [Kraichnan method][kraichnan_link], incompressible random +spatial vector fields can be generated. + + +### Example + +```python +import numpy as np +import gstools as gs +x = np.arange(100) +y = np.arange(100) +model = gs.Gaussian(dim=2, var=1, len_scale=10) +srf = gs.SRF(model, generator='VectorField', seed=19841203) +srf((x, y), mesh_type='structured') +srf.plot() +``` + +yielding + +

+vector field +

+ + +[kraichnan_link]: https://doi.org/10.1063/1.1692799 + + +## Plurigaussian Field Simulation (PGS) + +With PGS, more complex categorical (or discrete) fields can be created. + + +### Example + +```python +import gstools as gs +import numpy as np +import matplotlib.pyplot as plt + +N = [180, 140] + +x, y = range(N[0]), range(N[1]) + +# we need 2 SRFs +model = gs.Gaussian(dim=2, var=1, len_scale=5) +srf = gs.SRF(model) +field1 = srf.structured([x, y], seed=20170519) +field2 = srf.structured([x, y], seed=19970221) + +# with `lithotypes`, we prescribe the categorical data and its relations +# here, we use 2 categories separated by a rectangle. +rect = [40, 32] +lithotypes = np.zeros(N) +lithotypes[ + N[0] // 2 - rect[0] // 2 : N[0] // 2 + rect[0] // 2, + N[1] // 2 - rect[1] // 2 : N[1] // 2 + rect[1] // 2, +] = 1 + +pgs = gs.PGS(2, [field1, field2]) +P = pgs(lithotypes) + +fig, axs = plt.subplots(1, 2) +axs[0].imshow(lithotypes, cmap="copper") +axs[1].imshow(P, cmap="copper") +plt.show() +``` + +

+PGS +

+ + +## VTK/PyVista Export + +After you have created a field, you may want to save it to file, so we provide +a handy [VTK][vtk_link] export routine using the `.vtk_export()` or you could +create a VTK/PyVista dataset for use in Python with to `.to_pyvista()` method: + +```python +import gstools as gs +x = y = range(100) +model = gs.Gaussian(dim=2, var=1, len_scale=10) +srf = gs.SRF(model) +srf((x, y), mesh_type='structured') +srf.vtk_export("field") # Saves to a VTK file +mesh = srf.to_pyvista() # Create a VTK/PyVista dataset in memory +mesh.plot() +``` + +Which gives a RectilinearGrid VTK file ``field.vtr`` or creates a PyVista mesh +in memory for immediate 3D plotting in Python. + +

+pyvista export +

+ + +## Requirements: + +- [NumPy >= 1.20.0](https://www.numpy.org) +- [SciPy >= 1.1.0](https://www.scipy.org/scipylib) +- [hankel >= 1.0.0](https://github.com/steven-murray/hankel) +- [emcee >= 3.0.0](https://github.com/dfm/emcee) +- [pyevtk >= 1.1.1](https://github.com/pyscience-projects/pyevtk) +- [meshio >= 5.1.0](https://github.com/nschloe/meshio) + +### Optional + +- [GSTools-Core >= 0.2.0](https://github.com/GeoStat-Framework/GSTools-Core) +- [matplotlib](https://matplotlib.org) +- [pyvista](https://docs.pyvista.org/) + + +## Contact + +You can contact us via . + + +## License + +[LGPLv3][license_link] © 2018-2025 + +[pip_link]: https://pypi.org/project/gstools +[conda_link]: https://docs.conda.io/en/latest/miniconda.html +[conda_forge_link]: https://github.com/conda-forge/gstools-feedstock#installing-gstools +[conda_pip]: https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-pkgs.html#installing-non-conda-packages +[pipiflag]: https://pip-python3.readthedocs.io/en/latest/reference/pip_install.html?highlight=i#cmdoption-i +[winpy_link]: https://winpython.github.io/ +[license_link]: https://github.com/GeoStat-Framework/GSTools/blob/main/LICENSE +[cov_link]: https://geostat-framework.readthedocs.io/projects/gstools/en/stable/generated/gstools.covmodel.CovModel.html#gstools.covmodel.CovModel +[stable_link]: https://en.wikipedia.org/wiki/Stable_distribution +[doc_link]: https://geostat-framework.readthedocs.io/projects/gstools/en/stable/ +[doc_install_link]: https://geostat-framework.readthedocs.io/projects/gstools/en/stable/#pip +[tut_link]: https://geostat-framework.readthedocs.io/projects/gstools/en/stable/tutorials.html +[tut1_link]: https://geostat-framework.readthedocs.io/projects/gstools/en/stable/examples/01_random_field/index.html +[tut2_link]: https://geostat-framework.readthedocs.io/projects/gstools/en/stable/examples/02_cov_model/index.html +[tut3_link]: https://geostat-framework.readthedocs.io/projects/gstools/en/stable/examples/03_variogram/index.html +[tut4_link]: https://geostat-framework.readthedocs.io/projects/gstools/en/stable/examples/04_vector_field/index.html +[tut5_link]: https://geostat-framework.readthedocs.io/projects/gstools/en/stable/examples/05_kriging/index.html +[tut6_link]: https://geostat-framework.readthedocs.io/projects/gstools/en/stable/examples/06_conditioned_fields/index.html +[tut7_link]: https://geostat-framework.readthedocs.io/projects/gstools/en/stable/examples/07_transformations/index.html +[tut8_link]: https://geostat-framework.readthedocs.io/projects/gstools/en/stable/examples/08_geo_coordinates/index.html +[tut9_link]: https://geostat-framework.readthedocs.io/projects/gstools/en/stable/examples/09_spatio_temporal/index.html +[tut10_link]: https://geostat-framework.readthedocs.io/projects/gstools/en/stable/examples/10_normalizer/index.html +[tut11_link]: https://geostat-framework.readthedocs.io/projects/gstools/en/stable/examples/11_plurigaussian/index.html +[tut0_link]: https://geostat-framework.readthedocs.io/projects/gstools/en/stable/examples/00_misc/index.html +[cor_link]: https://en.wikipedia.org/wiki/Autocovariance#Normalization +[vtk_link]: https://www.vtk.org/ diff --git a/GSTools/source/__init__.py b/GSTools/source/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c8541bfc4bcb0b7e693a3f57c7905a620e8fab1a --- /dev/null +++ b/GSTools/source/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +""" +GSTools Project Package Initialization File +""" diff --git a/GSTools/source/docs/Makefile b/GSTools/source/docs/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..d1a288eba75e7b74d755ee478626165460bb3a0e --- /dev/null +++ b/GSTools/source/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = python3 -msphinx +SPHINXPROJ = GSTools +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/GSTools/source/docs/source/_static/custom.css b/GSTools/source/docs/source/_static/custom.css new file mode 100644 index 0000000000000000000000000000000000000000..e02ca17a1a8ead27536b1c1910a6dea20aed3e84 --- /dev/null +++ b/GSTools/source/docs/source/_static/custom.css @@ -0,0 +1,3 @@ +dl.py.property { + display: block !important; +} diff --git a/GSTools/source/docs/source/_templates/autosummary/class.rst b/GSTools/source/docs/source/_templates/autosummary/class.rst new file mode 100644 index 0000000000000000000000000000000000000000..c5c858a19b93a44650f37b375e09809270111129 --- /dev/null +++ b/GSTools/source/docs/source/_templates/autosummary/class.rst @@ -0,0 +1,13 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + :members: + :undoc-members: + :inherited-members: + :show-inheritance: + +.. raw:: latex + + \clearpage diff --git a/GSTools/source/docs/source/_templates/autosummary/module.rst b/GSTools/source/docs/source/_templates/autosummary/module.rst new file mode 100644 index 0000000000000000000000000000000000000000..04aefe90368c07940c25ed15413e600c1760a77e --- /dev/null +++ b/GSTools/source/docs/source/_templates/autosummary/module.rst @@ -0,0 +1,9 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ fullname }} + +.. automodule:: {{ fullname }} + +.. raw:: latex + + \clearpage diff --git a/GSTools/source/docs/source/_templates/layout.html b/GSTools/source/docs/source/_templates/layout.html new file mode 100644 index 0000000000000000000000000000000000000000..54ceb56b198404041cfd15eaf5cdd86a23b38513 --- /dev/null +++ b/GSTools/source/docs/source/_templates/layout.html @@ -0,0 +1,27 @@ +{% extends "!layout.html" %} + {% block menu %} + + {{ super() }} +
+ + +
+ + +
+
+ + {% endblock %} diff --git a/GSTools/source/docs/source/api.rst b/GSTools/source/docs/source/api.rst new file mode 100644 index 0000000000000000000000000000000000000000..fe12233b09b7d6b50fd4b249623214e403757662 --- /dev/null +++ b/GSTools/source/docs/source/api.rst @@ -0,0 +1,9 @@ +=========== +GSTools API +=========== + +.. automodule:: gstools + +.. raw:: latex + + \clearpage diff --git a/GSTools/source/docs/source/authors.rst b/GSTools/source/docs/source/authors.rst new file mode 100644 index 0000000000000000000000000000000000000000..49b950323fedeeeb9a1a4442c0ef52043a710c03 --- /dev/null +++ b/GSTools/source/docs/source/authors.rst @@ -0,0 +1,2 @@ +.. include:: ../../AUTHORS.md + :parser: myst_parser.docutils_ diff --git a/GSTools/source/docs/source/changelog.rst b/GSTools/source/docs/source/changelog.rst new file mode 100644 index 0000000000000000000000000000000000000000..a01bcd9f83885e230848b8d5aea3fc0cc7518bf3 --- /dev/null +++ b/GSTools/source/docs/source/changelog.rst @@ -0,0 +1,2 @@ +.. include:: ../../CHANGELOG.md + :parser: myst_parser.docutils_ diff --git a/GSTools/source/docs/source/conf.py b/GSTools/source/docs/source/conf.py new file mode 100644 index 0000000000000000000000000000000000000000..aaddc29f3fdacef251d027187d563a2d49423230 --- /dev/null +++ b/GSTools/source/docs/source/conf.py @@ -0,0 +1,338 @@ +#!/usr/bin/env python3 +# +# GSTools documentation build configuration file, created by +# sphinx-quickstart on Fri Jan 5 14:20:43 2018. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# NOTE: +# pip install sphinx_rtd_theme +# is needed in order to build the documentation +# import os +# import sys +import datetime +import warnings + +warnings.filterwarnings( + "ignore", + category=UserWarning, + message="Matplotlib is currently using agg, which is a non-GUI backend, so cannot show the figure.", +) + +# local module should not be added to sys path if it's installed on RTFD +# see: https://stackoverflow.com/a/31882049/6696397 +# sys.path.insert(0, os.path.abspath("../../")) +from gstools import __version__ as ver + + +def skip(app, what, name, obj, skip, options): + if name in ["__call__"]: + return False + return skip + + +def setup(app): + app.connect("autodoc-skip-member", skip) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.coverage", + "sphinx.ext.mathjax", + "sphinx.ext.ifconfig", + "sphinx.ext.viewcode", + "sphinx.ext.autosummary", + "sphinx.ext.napoleon", # parameters look better than with numpydoc only + "numpydoc", + "sphinx_gallery.gen_gallery", + "myst_parser", + "sphinxcontrib.youtube", +] + +# autosummaries from source-files +autosummary_generate = True +# dont show __init__ docstring +autoclass_content = "class" +# sort class members +autodoc_member_order = "groupwise" +# autodoc_member_order = 'bysource' + +# Notes in boxes +napoleon_use_admonition_for_notes = True +# Attributes like parameters +napoleon_use_ivar = True +# keep "Other Parameters" section +# https://github.com/sphinx-doc/sphinx/issues/10330 +napoleon_use_param = False +# this is a nice class-doc layout +numpydoc_show_class_members = True +# class members have no separate file, so they are not in a toctree +numpydoc_class_members_toctree = False +# for the covmodels alot of classmembers show up... +# maybe switch off with: :no-inherited-members: +numpydoc_show_inherited_class_members = True +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +source_suffix = { + ".rst": "restructuredtext", + ".md": "markdown", +} +# source_suffix = [".rst", ".md"] +# source_suffix = ".rst" + +# The master toctree document. +# --> this is the sitemap (or content-list in latex -> needs a heading) +# for html: the quickstart (in index.rst) +# gets the "index.html" and is therefore opened first +master_doc = "contents" + +# General information about the project. +curr_year = datetime.datetime.now().year +project = "GSTools" +copyright = f"2018 - {curr_year}, Sebastian Müller, Lennart Schüler" +author = "Sebastian Müller, Lennart Schüler" + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = ver +# The full version, including alpha/beta/rc tags. +release = ver + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = "en" + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "sphinx_rtd_theme" + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +html_theme_options = { + # 'canonical_url': '', + # 'analytics_id': '', + "logo_only": False, + "version_selector": True, + "prev_next_buttons_location": "top", + # 'style_external_links': False, + # 'vcs_pageview_mode': '', + # Toc options + "collapse_navigation": False, + "sticky_navigation": True, + "navigation_depth": 6, + "includehidden": True, + "titles_only": False, +} +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +# These paths are either relative to html_static_path +# or fully qualified paths (eg. https://...) +html_css_files = ["custom.css"] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# This is required for the alabaster theme +# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars +html_sidebars = { + "**": [ + "relations.html", # needs 'show_related': True theme option to display + "searchbox.html", + ] +} + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = "GSToolsdoc" +# logos for the page +html_logo = "pics/gstools_150.png" +html_favicon = "pics/gstools.ico" + +# -- Options for LaTeX output --------------------------------------------- +# latex_engine = 'lualatex' +# logo too big +latex_logo = "pics/gstools_150.png" + +# latex_show_urls = 'footnote' +# http://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-latex-output +latex_elements = { + "preamble": r""" +\setcounter{secnumdepth}{1} +\setcounter{tocdepth}{2} +\pagestyle{fancy} +""", + "pointsize": "10pt", + "papersize": "a4paper", + "fncychap": "\\usepackage[Glenn]{fncychap}", + # 'inputenc': r'\usepackage[utf8]{inputenc}', +} +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ( + master_doc, + "GSTools.tex", + "GSTools Documentation", + "Sebastian Müller, Lennart Schüler", + "manual", + ) +] +# latex_use_parts = True + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [(master_doc, "GSTools", "GSTools Documentation", [author], 1)] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + master_doc, + "GSTools", + "GSTools Documentation", + author, + "GSTools", + "Geo-statistical toolbox.", + "Miscellaneous", + ) +] + +suppress_warnings = [ + "image.nonlocal_uri", + # 'app.add_directive', # this evtl. suppresses the numpydoc induced warning +] + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + "Python": ("https://docs.python.org/", None), + "NumPy": ("https://numpy.org/doc/stable/", None), + "SciPy": ("https://docs.scipy.org/doc/scipy/", None), + "matplotlib": ("https://matplotlib.org/stable/", None), + "hankel": ("https://hankel.readthedocs.io/en/latest/", None), + "emcee": ("https://emcee.readthedocs.io/en/latest/", None), +} + +# -- Sphinx Gallery Options +from sphinx_gallery.sorting import FileNameSortKey + +# Use pyvista's image scraper for example gallery +# import pyvista +# https://github.com/tkoyama010/pyvista-doc-translations/blob/85c835a3ada3a2adefac06ba70e15a101ffa9162/conf.py#L21 +# https://github.com/simpeg/discretize/blob/f414dd7ee7c5ba9a141cb2c37d4b71fdc531eae8/docs/conf.py#L334 +# Make sure off screen is set to true when building locally +# pyvista.OFF_SCREEN = True +# # necessary when building the sphinx gallery +# pyvista.BUILDING_GALLERY = True +# # Optional - set parameters like theme or window size +# pyvista.set_plot_theme("document") + +sphinx_gallery_conf = { + # "image_scrapers": ("pyvista", "matplotlib"), + "remove_config_comments": True, + # only show "print" output as output + "capture_repr": (), + # path to your examples scripts + "examples_dirs": [ + "../../examples/00_misc/", + "../../examples/01_random_field/", + "../../examples/02_cov_model/", + "../../examples/03_variogram/", + "../../examples/04_vector_field/", + "../../examples/05_kriging/", + "../../examples/06_conditioned_fields/", + "../../examples/07_transformations/", + "../../examples/08_geo_coordinates/", + "../../examples/09_spatio_temporal/", + "../../examples/10_normalizer/", + "../../examples/11_plurigaussian/", + "../../examples/12_sum_model/", + ], + # path where to save gallery generated examples + "gallery_dirs": [ + "examples/00_misc/", + "examples/01_random_field/", + "examples/02_cov_model/", + "examples/03_variogram/", + "examples/04_vector_field/", + "examples/05_kriging/", + "examples/06_conditioned_fields/", + "examples/07_transformations/", + "examples/08_geo_coordinates/", + "examples/09_spatio_temporal/", + "examples/10_normalizer/", + "examples/11_plurigaussian/", + "examples/12_sum_model/", + ], + # Pattern to search for example files + "filename_pattern": r"\.py", + # Remove the "Download all examples" button from the top level gallery + "download_all_examples": False, + # Sort gallery example by file name instead of number of lines (default) + "within_subsection_order": FileNameSortKey, + # directory where function granular galleries are stored + "backreferences_dir": "examples/backreferences", + # Modules for which function level galleries are created. In + "doc_module": "gstools", + # "first_notebook_cell": ( + # "%matplotlib inline\n" + # "from pyvista import set_plot_theme\n" + # "set_plot_theme('document')" + # ), + "matplotlib_animations": True, +} diff --git a/GSTools/source/docs/source/contents.rst b/GSTools/source/docs/source/contents.rst new file mode 100644 index 0000000000000000000000000000000000000000..b6979a5ce4145744bbb048349649cf39564340de --- /dev/null +++ b/GSTools/source/docs/source/contents.rst @@ -0,0 +1,13 @@ +======== +Contents +======== + +.. toctree:: + :includehidden: + :maxdepth: 3 + + index + tutorials + api + authors + changelog diff --git a/GSTools/source/docs/source/index.rst b/GSTools/source/docs/source/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..300d4e38f0b4a396c4e9c4c3526083b947c99acb --- /dev/null +++ b/GSTools/source/docs/source/index.rst @@ -0,0 +1,504 @@ +================== +GSTools Quickstart +================== + +.. image:: https://raw.githubusercontent.com/GeoStat-Framework/GSTools/main/docs/source/pics/gstools.png + :width: 150px + :align: center + +.. only:: html + + **Get in Touch!** + + |GH-Discussions| |Email| + + **Youtube Tutorial on GSTools** + + .. youtube:: qZBJ-AZXq6Q + :width: 100% + + | + +Purpose +======= + +GSTools provides geostatistical tools for various purposes: + +- random field generation, including periodic boundaries +- simple, ordinary, universal and external drift kriging +- conditioned field generation +- incompressible random vector field generation +- (automated) variogram estimation and fitting +- directional variogram estimation and modelling +- data normalization and transformation +- many readily provided and even user-defined covariance models +- metric spatio-temporal modelling +- plurigaussian field simulations (PGS) +- plotting and exporting routines + + +Installation +============ + +conda +----- + +GSTools can be installed via +`conda `_ on Linux, Mac, and +Windows. +Install the package by typing the following command in a command terminal: + +.. code-block:: none + + conda install gstools + +In case conda forge is not set up for your system yet, see the easy to follow +instructions on `conda forge `_. +Using conda, the parallelized version of GSTools should be installed. + + +pip +--- + +GSTools can be installed via `pip `_ +on Linux, Mac, and Windows. +On Windows you can install `WinPython `_ to get +Python and pip running. +Install the package by typing the following into command in a command terminal: + +.. code-block:: none + + pip install gstools + +To get the latest development version you can install it directly from GitHub: + +.. code-block:: none + + pip install git+git://github.com/GeoStat-Framework/GSTools.git@main + +If something went wrong during installation, try the :code:`-I` `flag from pip `_. + +**Speeding up GSTools by parallelization** + +We provide two possibilities to run GSTools in parallel, often causing a +massive improvement in runtime. In either case, the number of parallel +threads can be set with the global variable `config.NUM_THREADS`. If not set, +all cores are used. +When using conda, the parallel version of GSTools is installed per default. + +***Parallelizing Cython*** + +For parallel support, the `GSTools-Cython `_ +backend needs to be compiled from source the following way: + +.. code-block:: none + + export GSTOOLS_BUILD_PARALLEL=1 + pip install --no-binary=gstools-cython gstools + +You have to provide a C compiler and OpenMP to compile GSTools-Cython with parallel support. +The feature is controlled by the environment variable +``GSTOOLS_BUILD_PARALLEL``, that can be ``0`` or ``1`` (interpreted as ``0`` if not present). +Note, that the ``--no-binary=gstools-cython`` option forces pip to not use a wheel +for the GSTools-Cython backend. + +For the development version, you can do almost the same: + +.. code-block:: none + + export GSTOOLS_BUILD_PARALLEL=1 + pip install git+git://github.com/GeoStat-Framework/GSTools-Cython.git@main + pip install git+git://github.com/GeoStat-Framework/GSTools.git@main + + +***Using GSTools-Core for parallelization and even more speed*** + +You can install the optional dependency `GSTools-Core `_, +which is a re-implementation of GSTools-Cython: + +.. code-block:: none + + pip install gstools[rust] + +or by manually installing the package + +.. code-block:: none + + pip install gstools-core + +The new package uses the language Rust and it should be safer and faster (in some cases by orders of magnitude). +Once the package GSTools-Core is available on your machine, it will be used by default. +In case you want to switch back to the Cython implementation, you can set +:code:`gstools.config.USE_GSTOOLS_CORE=False` in your code. This also works at runtime. + +GSTools-Core will automatically run in parallel, without having to provide OpenMP or a local C compiler. + + +Citation +======== + +If you are using GSTools in your publication please cite our paper: + + Müller, S., Schüler, L., Zech, A., and Heße, F.: GSTools v1.3: a toolbox for geostatistical modelling in Python, Geosci. Model Dev., 15, 3161–3182, https://doi.org/10.5194/gmd-15-3161-2022, 2022. + +You can cite the Zenodo code publication of GSTools by: + + Sebastian Müller & Lennart Schüler. GeoStat-Framework/GSTools. Zenodo. https://doi.org/10.5281/zenodo.1313628 + +If you want to cite a specific version, have a look at the `Zenodo site `__. + + +Tutorials and Examples +====================== + +The documentation also includes some `tutorials `__, +showing the most important use cases of GSTools, which are + +- `Random Field Generation `__ +- `The Covariance Model `__ +- `Variogram Estimation `__ +- `Random Vector Field Generation `__ +- `Kriging `__ +- `Conditioned random field generation `__ +- `Field transformations `__ +- `Geographic Coordinates `__ +- `Spatio-Temporal Modelling `__ +- `Normalizing Data `__ +- `Plurigaussian Field Generation (PGS) `__ +- `Miscellaneous examples `__ + + +Spatial Random Field Generation +=============================== + +The core of this library is the generation of spatial random fields. +These fields are generated using the randomisation method, described by +`Heße et al. 2014 `_. + + +Examples +-------- + +Gaussian Covariance Model +^^^^^^^^^^^^^^^^^^^^^^^^^ + +This is an example of how to generate a 2 dimensional spatial random field (:any:`SRF`) +with a :any:`Gaussian` covariance model. + +.. code-block:: python + + import gstools as gs + # structured field with a size 100x100 and a grid-size of 1x1 + x = y = range(100) + model = gs.Gaussian(dim=2, var=1, len_scale=10) + srf = gs.SRF(model) + srf((x, y), mesh_type='structured') + srf.plot() + +.. image:: https://raw.githubusercontent.com/GeoStat-Framework/GSTools/main/docs/source/pics/gau_field.png + :width: 400px + :align: center + +GSTools also provides support for `geographic coordinates `_. +This works perfectly well with `cartopy `_. + +.. code-block:: python + + import matplotlib.pyplot as plt + import cartopy.crs as ccrs + import gstools as gs + # define a structured field by latitude and longitude + lat = lon = range(-80, 81) + model = gs.Gaussian(latlon=True, len_scale=777, geo_scale=gs.KM_SCALE) + srf = gs.SRF(model, seed=12345) + field = srf.structured((lat, lon)) + # Orthographic plotting with cartopy + ax = plt.subplot(projection=ccrs.Orthographic(-45, 45)) + cont = ax.contourf(lon, lat, field, transform=ccrs.PlateCarree()) + ax.coastlines() + ax.set_global() + plt.colorbar(cont) + +.. image:: https://github.com/GeoStat-Framework/GeoStat-Framework.github.io/raw/master/img/GS_globe.png + :width: 400px + :align: center + +A similar example but for a three dimensional field is exported to a +`VTK `__ file, which can be visualized with +`ParaView `_ or +`PyVista `__ in Python: + +.. code-block:: python + + import gstools as gs + # structured field with a size 100x100x100 and a grid-size of 1x1x1 + x = y = z = range(100) + model = gs.Gaussian(dim=3, len_scale=[16, 8, 4], angles=(0.8, 0.4, 0.2)) + srf = gs.SRF(model) + srf((x, y, z), mesh_type='structured') + srf.vtk_export('3d_field') # Save to a VTK file for ParaView + + mesh = srf.to_pyvista() # Create a PyVista mesh for plotting in Python + mesh.contour(isosurfaces=8).plot() + +.. image:: https://github.com/GeoStat-Framework/GeoStat-Framework.github.io/raw/master/img/GS_pyvista.png + :width: 400px + :align: center + + +Estimating and fitting variograms +================================= + +The spatial structure of a field can be analyzed with the variogram, which contains the same information as the covariance function. + +All covariance models can be used to fit given variogram data by a simple interface. + + +Examples +-------- + +This is an example of how to estimate the variogram of a 2 dimensional unstructured field and estimate the parameters of the covariance +model again. + +.. code-block:: python + + import numpy as np + import gstools as gs + # generate a synthetic field with an exponential model + x = np.random.RandomState(19970221).rand(1000) * 100. + y = np.random.RandomState(20011012).rand(1000) * 100. + model = gs.Exponential(dim=2, var=2, len_scale=8) + srf = gs.SRF(model, mean=0, seed=19970221) + field = srf((x, y)) + # estimate the variogram of the field + bin_center, gamma = gs.vario_estimate((x, y), field) + # fit the variogram with a stable model. (no nugget fitted) + fit_model = gs.Stable(dim=2) + fit_model.fit_variogram(bin_center, gamma, nugget=False) + # output + ax = fit_model.plot(x_max=max(bin_center)) + ax.scatter(bin_center, gamma) + print(fit_model) + +Which gives: + +.. code-block:: python + + Stable(dim=2, var=1.85, len_scale=7.42, nugget=0.0, anis=[1.0], angles=[0.0], alpha=1.09) + +.. image:: https://raw.githubusercontent.com/GeoStat-Framework/GeoStat-Framework.github.io/master/img/GS_vario_est.png + :width: 400px + :align: center + + +Kriging and Conditioned Random Fields +===================================== + +An important part of geostatistics is Kriging and conditioning spatial random +fields to measurements. With conditioned random fields, an ensemble of field realizations +with their variability depending on the proximity of the measurements can be generated. + + +Example +------- + +For better visualization, we will condition a 1d field to a few "measurements", +generate 100 realizations and plot them: + +.. code-block:: python + + import numpy as np + import matplotlib.pyplot as plt + import gstools as gs + + # conditions + cond_pos = [0.3, 1.9, 1.1, 3.3, 4.7] + cond_val = [0.47, 0.56, 0.74, 1.47, 1.74] + + # conditioned spatial random field class + model = gs.Gaussian(dim=1, var=0.5, len_scale=2) + krige = gs.krige.Ordinary(model, cond_pos, cond_val) + cond_srf = gs.CondSRF(krige) + # same output positions for all ensemble members + grid_pos = np.linspace(0.0, 15.0, 151) + cond_srf.set_pos(grid_pos) + + # seeded ensemble generation + seed = gs.random.MasterRNG(20170519) + for i in range(100): + field = cond_srf(seed=seed(), store=f"field_{i}") + plt.plot(grid_pos, field, color="k", alpha=0.1) + plt.scatter(cond_pos, cond_val, color="k") + plt.show() + +.. image:: https://raw.githubusercontent.com/GeoStat-Framework/GSTools/main/docs/source/pics/cond_ens.png + :width: 600px + :align: center + + +User defined covariance models +============================== + +One of the core-features of GSTools is the powerful +:any:`CovModel` +class, which allows to easy define covariance models by the user. + + +Example +------- + +Here we re-implement the Gaussian covariance model by defining just the +`correlation `_ function, +which takes a non-dimensional distance :class:`h = r/l` + +.. code-block:: python + + import numpy as np + import gstools as gs + # use CovModel as the base-class + class Gau(gs.CovModel): + def cor(self, h): + return np.exp(-h**2) + +And that's it! With :class:`Gau` you now have a fully working covariance model, +which you could use for field generation or variogram fitting as shown above. + + +Incompressible Vector Field Generation +====================================== + +Using the original `Kraichnan method `_, incompressible random +spatial vector fields can be generated. + + +Example +------- + +.. code-block:: python + + import numpy as np + import gstools as gs + x = np.arange(100) + y = np.arange(100) + model = gs.Gaussian(dim=2, var=1, len_scale=10) + srf = gs.SRF(model, generator='VectorField', seed=19841203) + srf((x, y), mesh_type='structured') + srf.plot() + +yielding + +.. image:: https://raw.githubusercontent.com/GeoStat-Framework/GSTools/main/docs/source/pics/vec_srf_tut_gau.png + :width: 600px + :align: center + + +Plurigaussian Field Simulation (PGS) +==================================== + +With PGS, more complex categorical (or discrete) fields can be created. + + +Example +------- + +.. code-block:: python + + import gstools as gs + import numpy as np + import matplotlib.pyplot as plt + + N = [180, 140] + + x, y = range(N[0]), range(N[1]) + + # we need 2 SRFs + model = gs.Gaussian(dim=2, var=1, len_scale=5) + srf = gs.SRF(model) + field1 = srf.structured([x, y], seed=20170519) + field2 = srf.structured([x, y], seed=19970221) + + # with `lithotypes`, we prescribe the categorical data and its relations + # here, we use 2 categories separated by a rectangle. + rect = [40, 32] + lithotypes = np.zeros(N) + lithotypes[ + N[0] // 2 - rect[0] // 2 : N[0] // 2 + rect[0] // 2, + N[1] // 2 - rect[1] // 2 : N[1] // 2 + rect[1] // 2, + ] = 1 + + pgs = gs.PGS(2, [field1, field2]) + P = pgs(lithotypes) + + fig, axs = plt.subplots(1, 2) + axs[0].imshow(lithotypes, cmap="copper") + axs[1].imshow(P, cmap="copper") + plt.show() + +.. image:: https://raw.githubusercontent.com/GeoStat-Framework/GSTools/main/docs/source/pics/2d_pgs.png + :width: 600px + :align: center + + +VTK/PyVista Export +================== + +After you have created a field, you may want to save it to file, so we provide +a handy `VTK `_ export routine using the :class:`.vtk_export()` or you could +create a VTK/PyVista dataset for use in Python with to :class:`.to_pyvista()` method: + +.. code-block:: python + + import gstools as gs + x = y = range(100) + model = gs.Gaussian(dim=2, var=1, len_scale=10) + srf = gs.SRF(model) + srf((x, y), mesh_type='structured') + srf.vtk_export("field") # Saves to a VTK file + mesh = srf.to_pyvista() # Create a VTK/PyVista dataset in memory + mesh.plot() + +Which gives a RectilinearGrid VTK file :file:`field.vtr` or creates a PyVista mesh +in memory for immediate 3D plotting in Python. + +.. image:: https://raw.githubusercontent.com/GeoStat-Framework/GSTools/main/docs/source/pics/pyvista_export.png + :width: 600px + :align: center + + +Requirements +============ + +- `NumPy >= 1.20.0 `_ +- `SciPy >= 1.1.0 `_ +- `hankel >= 1.0.0 `_ +- `emcee >= 3.0.0 `_ +- `pyevtk >= 1.1.1 `_ +- `meshio >= 5.1.0 `_ + + +Optional +-------- + +- `GSTools-Core >= 0.2.0 `_ +- `matplotlib `_ +- `pyvista `_ + + +Contact +------- + +You can contact us via `info@geostat-framework.org `_. + + +License +======= + +`LGPLv3 `_ + + +.. |GH-Discussions| image:: https://img.shields.io/badge/GitHub-Discussions-f6f8fa?logo=github&style=flat + :alt: GH-Discussions + :target: https://github.com/GeoStat-Framework/GSTools/discussions +.. |Email| image:: https://img.shields.io/badge/Email-GeoStat--Framework-468a88?style=flat&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHdpZHRoPSI1MDAiIGhlaWdodD0iNTAwIj48cGF0aCBkPSJNNDQ4IDg4SDUyYy0yNyAwLTQ5IDIyLTQ5IDQ5djIyNmMwIDI3IDIyIDQ5IDQ5IDQ5aDM5NmMyNyAwIDQ5LTIyIDQ5LTQ5VjEzN2MwLTI3LTIyLTQ5LTQ5LTQ5em0xNiA0OXYyMjZsLTIgNy0xMTUtMTE2IDExNy0xMTd6TTM2IDM2M1YxMzdsMTE3IDExN0wzOCAzNzBsLTItN3ptMjE5LTYzYy0zIDMtNyAzLTEwIDBMNjYgMTIxaDM2OHptLTc5LTIzIDQ2IDQ2YTM5IDM5IDAgMCAwIDU2IDBsNDYtNDYgMTAxIDEwMkg3NXoiIHN0eWxlPSJmaWxsOiNmNWY1ZjU7ZmlsbC1vcGFjaXR5OjEiLz48L3N2Zz4= + :alt: Email + :target: mailto:info@geostat-framework.org diff --git a/GSTools/source/docs/source/pics/05_ordinary.png b/GSTools/source/docs/source/pics/05_ordinary.png new file mode 100644 index 0000000000000000000000000000000000000000..8a529968b9b97dc1f6bdaa9029abc97dc55fb388 Binary files /dev/null and b/GSTools/source/docs/source/pics/05_ordinary.png differ diff --git a/GSTools/source/docs/source/pics/05_simple.png b/GSTools/source/docs/source/pics/05_simple.png new file mode 100644 index 0000000000000000000000000000000000000000..fad8c8bebc54fd7589c86272399e588db74e863e Binary files /dev/null and b/GSTools/source/docs/source/pics/05_simple.png differ diff --git a/GSTools/source/docs/source/pics/06_ensemble.png b/GSTools/source/docs/source/pics/06_ensemble.png new file mode 100644 index 0000000000000000000000000000000000000000..ea563f71f26e99ad3f23ee909a9989780622d526 --- /dev/null +++ b/GSTools/source/docs/source/pics/06_ensemble.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4c00c0835fe2385dfa97514a6b2e02875f38c01c337c43fbde5b7540c3f3df6 +size 247300 diff --git a/GSTools/source/docs/source/pics/07_00_std.png b/GSTools/source/docs/source/pics/07_00_std.png new file mode 100644 index 0000000000000000000000000000000000000000..dedd1500e99f5a443f1a23c9dd9ab1b75c43ea6c --- /dev/null +++ b/GSTools/source/docs/source/pics/07_00_std.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a81c3a07ec10a5efca9a16c804904fab1c6f9bad84bd1acfb6edb0394caed0ac +size 116115 diff --git a/GSTools/source/docs/source/pics/07_01_lognormal.png b/GSTools/source/docs/source/pics/07_01_lognormal.png new file mode 100644 index 0000000000000000000000000000000000000000..430327ace770cc53bf7864d87a7efffb97ffd6d2 Binary files /dev/null and b/GSTools/source/docs/source/pics/07_01_lognormal.png differ diff --git a/GSTools/source/docs/source/pics/07_02_binary.png b/GSTools/source/docs/source/pics/07_02_binary.png new file mode 100644 index 0000000000000000000000000000000000000000..7d720f175494c292dccf144073c3971d6f699417 Binary files /dev/null and b/GSTools/source/docs/source/pics/07_02_binary.png differ diff --git a/GSTools/source/docs/source/pics/07_03_zinnharvey.png b/GSTools/source/docs/source/pics/07_03_zinnharvey.png new file mode 100644 index 0000000000000000000000000000000000000000..52f1e3540b9432050ca77f3dc011d2c1ad8aa69a --- /dev/null +++ b/GSTools/source/docs/source/pics/07_03_zinnharvey.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4dbc33ee528858a37b27e5f7aaeb3cf65a0f47246baed12b9787c17848f2576 +size 157566 diff --git a/GSTools/source/docs/source/pics/07_04_arcsin.png b/GSTools/source/docs/source/pics/07_04_arcsin.png new file mode 100644 index 0000000000000000000000000000000000000000..420fe156fbbe20777287477ce452805503c6049a --- /dev/null +++ b/GSTools/source/docs/source/pics/07_04_arcsin.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8c175a272e71dd8eefc84d942f036578cfa7271b439721f431dbaf41345a27e +size 150476 diff --git a/GSTools/source/docs/source/pics/07_05_combine.png b/GSTools/source/docs/source/pics/07_05_combine.png new file mode 100644 index 0000000000000000000000000000000000000000..bcd9222e6e6f6ae09ffcc684e6d924d92bfdde27 Binary files /dev/null and b/GSTools/source/docs/source/pics/07_05_combine.png differ diff --git a/GSTools/source/docs/source/pics/09_cond_ens.png b/GSTools/source/docs/source/pics/09_cond_ens.png new file mode 100644 index 0000000000000000000000000000000000000000..de90b9f1cbdb71bd7abbe8098a0c39238f7e1f8e --- /dev/null +++ b/GSTools/source/docs/source/pics/09_cond_ens.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5528cbf44e890306259539b2602e7e24df470929cc5c163bc9bc022ed7514c2f +size 176133 diff --git a/GSTools/source/docs/source/pics/20_gstools.png b/GSTools/source/docs/source/pics/20_gstools.png new file mode 100644 index 0000000000000000000000000000000000000000..6f20910274420d3087887eaf69ddd43a6628c353 Binary files /dev/null and b/GSTools/source/docs/source/pics/20_gstools.png differ diff --git a/GSTools/source/docs/source/pics/20_pykrige.png b/GSTools/source/docs/source/pics/20_pykrige.png new file mode 100644 index 0000000000000000000000000000000000000000..fa5451463062bafd8857df2e7810a817ea174b7c Binary files /dev/null and b/GSTools/source/docs/source/pics/20_pykrige.png differ diff --git a/GSTools/source/docs/source/pics/2d_pgs.png b/GSTools/source/docs/source/pics/2d_pgs.png new file mode 100644 index 0000000000000000000000000000000000000000..61551681c37e18d2b3c428fdb86d13558c888255 Binary files /dev/null and b/GSTools/source/docs/source/pics/2d_pgs.png differ diff --git a/GSTools/source/docs/source/pics/3d_gau_field.png b/GSTools/source/docs/source/pics/3d_gau_field.png new file mode 100644 index 0000000000000000000000000000000000000000..c8b0ae8b7fbfa97f3ed49edfb5264790e9e58686 --- /dev/null +++ b/GSTools/source/docs/source/pics/3d_gau_field.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d60a80aec34ac7c1e45ffd0be9c0dde414d10af9469e9ed116d572c51938007a +size 374614 diff --git a/GSTools/source/docs/source/pics/3d_pgs.png b/GSTools/source/docs/source/pics/3d_pgs.png new file mode 100644 index 0000000000000000000000000000000000000000..3987fd7b1b0094023ad7275e172fc4c72dc78817 --- /dev/null +++ b/GSTools/source/docs/source/pics/3d_pgs.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11abc212d8947e775043553efce569a79455344baf6922e1d76844adf3d804c3 +size 140476 diff --git a/GSTools/source/docs/source/pics/GS_3d_vector_field.png b/GSTools/source/docs/source/pics/GS_3d_vector_field.png new file mode 100644 index 0000000000000000000000000000000000000000..05a15a47904a9897986b799e72cbd17331b67ac3 --- /dev/null +++ b/GSTools/source/docs/source/pics/GS_3d_vector_field.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1caf236fedfe6d8d006be9c6aa168fcdf1172f68b2a90f38254fcd44e85b748f +size 1058528 diff --git a/GSTools/source/docs/source/pics/GS_pyvista_cut.png b/GSTools/source/docs/source/pics/GS_pyvista_cut.png new file mode 100644 index 0000000000000000000000000000000000000000..ef660778960789fd5231188799f88e39a5f6e35a --- /dev/null +++ b/GSTools/source/docs/source/pics/GS_pyvista_cut.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50dc31acc19648cf95a9206328b83f263e414cbc71a62aad7fabf1c23b15cb3c +size 368144 diff --git a/GSTools/source/docs/source/pics/cond_ens.png b/GSTools/source/docs/source/pics/cond_ens.png new file mode 100644 index 0000000000000000000000000000000000000000..afc56cdd4fffe2a20e5b12dac1282330bf4e4287 --- /dev/null +++ b/GSTools/source/docs/source/pics/cond_ens.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc7ad2082ff7d60ae90bbab31e87f52e49d9be39246f105769ce81b2d56189ce +size 104916 diff --git a/GSTools/source/docs/source/pics/cov_model_vario.png b/GSTools/source/docs/source/pics/cov_model_vario.png new file mode 100644 index 0000000000000000000000000000000000000000..7dbbe9ec1c61b8dd9b847284fa17c4db0c6f3857 Binary files /dev/null and b/GSTools/source/docs/source/pics/cov_model_vario.png differ diff --git a/GSTools/source/docs/source/pics/demonstrator.png b/GSTools/source/docs/source/pics/demonstrator.png new file mode 100644 index 0000000000000000000000000000000000000000..395a317e22aeba0859b5e6de7a928e39ae76afbd Binary files /dev/null and b/GSTools/source/docs/source/pics/demonstrator.png differ diff --git a/GSTools/source/docs/source/pics/exp_vario_fit.png b/GSTools/source/docs/source/pics/exp_vario_fit.png new file mode 100644 index 0000000000000000000000000000000000000000..24ec179961f41c326878de3041bc405618a97501 Binary files /dev/null and b/GSTools/source/docs/source/pics/exp_vario_fit.png differ diff --git a/GSTools/source/docs/source/pics/gau_field.png b/GSTools/source/docs/source/pics/gau_field.png new file mode 100644 index 0000000000000000000000000000000000000000..c00f7b83f7fb2ae6ddbdfa42a17b9f1e77979016 --- /dev/null +++ b/GSTools/source/docs/source/pics/gau_field.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e26c5d8592b6af07bad00f0aa374a0b16633b9803672a326bb0d17c0662c29c7 +size 117912 diff --git a/GSTools/source/docs/source/pics/gstools.ico b/GSTools/source/docs/source/pics/gstools.ico new file mode 100644 index 0000000000000000000000000000000000000000..9119caaa6da28b9936666b9421f91e5d5ede3058 Binary files /dev/null and b/GSTools/source/docs/source/pics/gstools.ico differ diff --git a/GSTools/source/docs/source/pics/gstools.png b/GSTools/source/docs/source/pics/gstools.png new file mode 100644 index 0000000000000000000000000000000000000000..5f4be62ce1415ef8e2c830af25c285be7b6d1b24 Binary files /dev/null and b/GSTools/source/docs/source/pics/gstools.png differ diff --git a/GSTools/source/docs/source/pics/gstools_150.png b/GSTools/source/docs/source/pics/gstools_150.png new file mode 100644 index 0000000000000000000000000000000000000000..d2a43d380def4ce33ee78675adc7b8c1c2ee5b94 Binary files /dev/null and b/GSTools/source/docs/source/pics/gstools_150.png differ diff --git a/GSTools/source/docs/source/pics/paraview.png b/GSTools/source/docs/source/pics/paraview.png new file mode 100644 index 0000000000000000000000000000000000000000..d94880628ae4cc72420fec9835c41619175454eb --- /dev/null +++ b/GSTools/source/docs/source/pics/paraview.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1694bae39eff4ecdbb0d3fb64a181e8c8a499f240a34ee4cb619acaf7c792b1c +size 448557 diff --git a/GSTools/source/docs/source/pics/pyvista_export.png b/GSTools/source/docs/source/pics/pyvista_export.png new file mode 100644 index 0000000000000000000000000000000000000000..a92401ee4030abfcf3aed3e1e7d057cc57458b6b --- /dev/null +++ b/GSTools/source/docs/source/pics/pyvista_export.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d4d0befe54aba0db8180d6fd41a14703e7b8ef26b794daa1be60b400059af70 +size 220188 diff --git a/GSTools/source/docs/source/pics/srf_tut_exp_ani_rot.png b/GSTools/source/docs/source/pics/srf_tut_exp_ani_rot.png new file mode 100644 index 0000000000000000000000000000000000000000..6cdc9c79b94952189ba3cd5f28bc8be38bdb13a4 --- /dev/null +++ b/GSTools/source/docs/source/pics/srf_tut_exp_ani_rot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed19d7affeb70dcde135dbc66cf8661c20aa82c15853bed654f73428b1164e02 +size 213828 diff --git a/GSTools/source/docs/source/pics/srf_tut_gau_field.png b/GSTools/source/docs/source/pics/srf_tut_gau_field.png new file mode 100644 index 0000000000000000000000000000000000000000..0b7f33df4303c3357050cdb52e04316e4bbc095c --- /dev/null +++ b/GSTools/source/docs/source/pics/srf_tut_gau_field.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93d7fc15a505483705250e0e7cb74f32bcace74ac0c2ca668c12985bbbfe593e +size 113149 diff --git a/GSTools/source/docs/source/pics/srf_tut_gau_field_ens.png b/GSTools/source/docs/source/pics/srf_tut_gau_field_ens.png new file mode 100644 index 0000000000000000000000000000000000000000..7a8ab35e3c27bf7aa8e3bcab5b2be7ba8fc10f1a Binary files /dev/null and b/GSTools/source/docs/source/pics/srf_tut_gau_field_ens.png differ diff --git a/GSTools/source/docs/source/pics/srf_tut_merge.png b/GSTools/source/docs/source/pics/srf_tut_merge.png new file mode 100644 index 0000000000000000000000000000000000000000..57474e514676854035253bea3f4553e85717c319 Binary files /dev/null and b/GSTools/source/docs/source/pics/srf_tut_merge.png differ diff --git a/GSTools/source/docs/source/pics/srf_tut_unstr.png b/GSTools/source/docs/source/pics/srf_tut_unstr.png new file mode 100644 index 0000000000000000000000000000000000000000..764b7592700780d5d7ef34822ad770b11d8d2afb --- /dev/null +++ b/GSTools/source/docs/source/pics/srf_tut_unstr.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec268f9e52e355293037045af0313a65ae6fc38eda0110bd8d2a67dd777244f6 +size 210287 diff --git a/GSTools/source/docs/source/pics/srf_vector_field.png b/GSTools/source/docs/source/pics/srf_vector_field.png new file mode 100644 index 0000000000000000000000000000000000000000..59d1bad21593d2c880494b7456d6d6beb4ee9ca7 Binary files /dev/null and b/GSTools/source/docs/source/pics/srf_vector_field.png differ diff --git a/GSTools/source/docs/source/pics/stab_vario_fit.png b/GSTools/source/docs/source/pics/stab_vario_fit.png new file mode 100644 index 0000000000000000000000000000000000000000..2b3c6e3c534facd4ba91d7e25956a6776f66f66f Binary files /dev/null and b/GSTools/source/docs/source/pics/stab_vario_fit.png differ diff --git a/GSTools/source/docs/source/pics/tplstable_field.png b/GSTools/source/docs/source/pics/tplstable_field.png new file mode 100644 index 0000000000000000000000000000000000000000..9d1790832bbcb44ffb0547dbeb01922fbd1121e5 Binary files /dev/null and b/GSTools/source/docs/source/pics/tplstable_field.png differ diff --git a/GSTools/source/docs/source/pics/vario_tut_aniso_fit_exp.png b/GSTools/source/docs/source/pics/vario_tut_aniso_fit_exp.png new file mode 100644 index 0000000000000000000000000000000000000000..8344e93bc217bef52734923f5a4a46c0682d217d Binary files /dev/null and b/GSTools/source/docs/source/pics/vario_tut_aniso_fit_exp.png differ diff --git a/GSTools/source/docs/source/pics/vario_tut_fit_exp.png b/GSTools/source/docs/source/pics/vario_tut_fit_exp.png new file mode 100644 index 0000000000000000000000000000000000000000..06fc66dbec03a6ea9245baed12dd13217248ce5e Binary files /dev/null and b/GSTools/source/docs/source/pics/vario_tut_fit_exp.png differ diff --git a/GSTools/source/docs/source/pics/vario_tut_herten.png b/GSTools/source/docs/source/pics/vario_tut_herten.png new file mode 100644 index 0000000000000000000000000000000000000000..ee66d97eabba12ee1f5328d53ba9e081dce4ca20 --- /dev/null +++ b/GSTools/source/docs/source/pics/vario_tut_herten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c10f65627529d7e6f204c3e921f4f062dd43712a68b10ed2a3b494ce24ca217 +size 645514 diff --git a/GSTools/source/docs/source/pics/vario_tut_new_herten.png b/GSTools/source/docs/source/pics/vario_tut_new_herten.png new file mode 100644 index 0000000000000000000000000000000000000000..9b4642c955d81d59b3503eefedd2499de16c802d --- /dev/null +++ b/GSTools/source/docs/source/pics/vario_tut_new_herten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce75c8a2d336a3e24d062c3b9a5cca54f211a31ca8622fcd3f2b68fbc060f0c8 +size 621374 diff --git a/GSTools/source/docs/source/pics/vec_srf_tut_exp.png b/GSTools/source/docs/source/pics/vec_srf_tut_exp.png new file mode 100644 index 0000000000000000000000000000000000000000..7c1b179bf6c1f0df529fd8b44f0c6c13984ba47a --- /dev/null +++ b/GSTools/source/docs/source/pics/vec_srf_tut_exp.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:056b21d8a2258ca7d8681bba0bfe0cf67ded14d3a0aa29f124a116da8eeb05b2 +size 155529 diff --git a/GSTools/source/docs/source/pics/vec_srf_tut_gau.png b/GSTools/source/docs/source/pics/vec_srf_tut_gau.png new file mode 100644 index 0000000000000000000000000000000000000000..6b6ddba55878a72513d0f0bc4c3b8fc9f0ad3a5a --- /dev/null +++ b/GSTools/source/docs/source/pics/vec_srf_tut_gau.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f109e988e6129f9be13b310bb4b63e45b88ff2582f25173b8c9c915ee45d76a8 +size 150536 diff --git a/GSTools/source/docs/source/tutorials.rst b/GSTools/source/docs/source/tutorials.rst new file mode 100644 index 0000000000000000000000000000000000000000..e3b20db1b8da1d11e81eb306ab00d9977c2cf8e1 --- /dev/null +++ b/GSTools/source/docs/source/tutorials.rst @@ -0,0 +1,43 @@ +.. _tutorials: + +================= +GSTools Tutorials +================= + +In the following you will find several Tutorials on how to use GSTools to +explore its whole beauty and power. + + +.. toctree:: + :includehidden: + :maxdepth: 1 + + examples/01_random_field/index + examples/02_cov_model/index + examples/03_variogram/index + examples/04_vector_field/index + examples/05_kriging/index + examples/06_conditioned_fields/index + examples/07_transformations/index + examples/08_geo_coordinates/index + examples/09_spatio_temporal/index + examples/10_normalizer/index + examples/11_plurigaussian/index + examples/12_sum_model/index + examples/00_misc/index + +.. only:: html + + **Youtube Tutorial on GSTools** + + .. youtube:: qZBJ-AZXq6Q + :width: 100% + + | + + Gallery + ======= + + .. minigallery:: + + ../../examples/**/*.py diff --git a/GSTools/source/examples/00_misc/00_tpl_stable.py b/GSTools/source/examples/00_misc/00_tpl_stable.py new file mode 100644 index 0000000000000000000000000000000000000000..474b0f5580769fa5c3cfc9a17b32dec562800624 --- /dev/null +++ b/GSTools/source/examples/00_misc/00_tpl_stable.py @@ -0,0 +1,62 @@ +r""" +Truncated Power Law Variograms +------------------------------ + +GSTools also implements truncated power law variograms, +which can be represented as a superposition of scale dependant modes +in form of standard variograms, which are truncated by +a lower- :math:`\ell_{\mathrm{low}}` and +an upper length-scale :math:`\ell_{\mathrm{up}}`. + +This example shows the truncated power law (:any:`TPLStable`) based on the +:any:`Stable` covariance model and is given by + +.. math:: + \gamma_{\ell_{\mathrm{low}},\ell_{\mathrm{up}}}(r) = + \intop_{\ell_{\mathrm{low}}}^{\ell_{\mathrm{up}}} + \gamma(r,\lambda) \frac{\rm d \lambda}{\lambda} + +with `Stable` modes on each scale: + +.. math:: + \gamma(r,\lambda) &= + \sigma^2(\lambda)\cdot\left(1- + \exp\left[- \left(\frac{r}{\lambda}\right)^{\alpha}\right] + \right)\\ + \sigma^2(\lambda) &= C\cdot\lambda^{2H} + +which gives Gaussian modes for ``alpha=2`` +or Exponential modes for ``alpha=1``. + +For :math:`\ell_{\mathrm{low}}=0` this results in: + +.. math:: + \gamma_{\ell_{\mathrm{up}}}(r) &= + \sigma^2_{\ell_{\mathrm{up}}}\cdot\left(1- + \frac{2H}{\alpha} \cdot + E_{1+\frac{2H}{\alpha}} + \left[\left(\frac{r}{\ell_{\mathrm{up}}}\right)^{\alpha}\right] + \right) \\ + \sigma^2_{\ell_{\mathrm{up}}} &= + C\cdot\frac{\ell_{\mathrm{up}}^{2H}}{2H} +""" + +import numpy as np + +import gstools as gs + +x = y = np.linspace(0, 100, 100) +model = gs.TPLStable( + dim=2, # spatial dimension + var=1, # variance (C is calculated internally, so variance is actually 1) + len_low=0, # lower truncation of the power law + len_scale=10, # length scale (a.k.a. range), len_up = len_low + len_scale + nugget=0.1, # nugget + anis=0.5, # anisotropy between main direction and transversal ones + angles=np.pi / 4, # rotation angles + alpha=1.5, # shape parameter from the stable model + hurst=0.7, # hurst coefficient from the power law +) +srf = gs.SRF(model, mean=1.0, seed=19970221) +srf.structured([x, y]) +srf.plot() diff --git a/GSTools/source/examples/00_misc/01_export.py b/GSTools/source/examples/00_misc/01_export.py new file mode 100644 index 0000000000000000000000000000000000000000..afd763b39def9a0371d1572185de6754c15ea3d0 --- /dev/null +++ b/GSTools/source/examples/00_misc/01_export.py @@ -0,0 +1,25 @@ +""" +Exporting Fields +---------------- + +GSTools provides simple exporting routines to convert generated fields to +`VTK `__ files. + +These can be viewed for example with `Paraview `__. +""" + +# sphinx_gallery_thumbnail_path = 'pics/paraview.png' +import gstools as gs + +x = y = range(100) +model = gs.Gaussian(dim=2, var=1, len_scale=10) +srf = gs.SRF(model) +field = srf((x, y), mesh_type="structured") +srf.vtk_export(filename="field") + +############################################################################### +# The result displayed with Paraview: +# +# .. image:: ../../pics/paraview.png +# :width: 400px +# :align: center diff --git a/GSTools/source/examples/00_misc/02_check_rand_meth_sampling.py b/GSTools/source/examples/00_misc/02_check_rand_meth_sampling.py new file mode 100644 index 0000000000000000000000000000000000000000..58d998b4dce0eb29015cf066bbea5c5214d9409e --- /dev/null +++ b/GSTools/source/examples/00_misc/02_check_rand_meth_sampling.py @@ -0,0 +1,68 @@ +""" +Check Random Sampling +--------------------- +""" + +import numpy as np +from matplotlib import pyplot as plt +from mpl_toolkits.mplot3d import Axes3D + +import gstools as gs + + +def norm_rad(vec): + """Direction on the unit sphere.""" + vec = np.array(vec, ndmin=2) + norm = np.zeros(vec.shape[1]) + for i in range(vec.shape[0]): + norm += vec[i] ** 2 + norm = np.sqrt(norm) + return np.einsum("j,ij->ij", 1 / norm, vec), norm + + +def plot_rand_meth_samples(generator): + """Plot the samples of the rand meth class.""" + norm, rad = norm_rad(generator._cov_sample) + + fig = plt.figure(figsize=(10, 4)) + + if generator.model.dim == 3: + ax = fig.add_subplot(121, projection=Axes3D.name) + u = np.linspace(0, 2 * np.pi, 100) + v = np.linspace(0, np.pi, 100) + x = np.outer(np.cos(u), np.sin(v)) + y = np.outer(np.sin(u), np.sin(v)) + z = np.outer(np.ones(np.size(u)), np.cos(v)) + ax.plot_surface(x, y, z, rstride=4, cstride=4, color="b", alpha=0.1) + ax.scatter(norm[0], norm[1], norm[2]) + elif generator.model.dim == 2: + ax = fig.add_subplot(121) + u = np.linspace(0, 2 * np.pi, 100) + x = np.cos(u) + y = np.sin(u) + ax.plot(x, y, color="b", alpha=0.1) + ax.scatter(norm[0], norm[1]) + ax.set_aspect("equal") + else: + ax = fig.add_subplot(121) + ax.bar(-1, np.sum(np.isclose(norm, -1)), color="C0") + ax.bar(1, np.sum(np.isclose(norm, 1)), color="C0") + ax.set_xticks([-1, 1]) + ax.set_xticklabels(("-1", "1")) + ax.set_title("Direction sampling") + + ax = fig.add_subplot(122) + x = np.linspace(0, 10 / generator.model.integral_scale) + y = generator.model.spectral_rad_pdf(x) + ax.plot(x, y, label="radial spectral density") + sample_in = np.sum(rad <= np.max(x)) + ax.hist(rad[rad <= np.max(x)], bins=sample_in // 50, density=True) + ax.set_xlim([0, np.max(x)]) + ax.set_title(f"Radius samples shown {sample_in}/{len(rad)}") + ax.legend() + plt.show() + + +model = gs.Stable(dim=3, alpha=1.5) +srf = gs.SRF(model, seed=2020) +plot_rand_meth_samples(srf.generator) diff --git a/GSTools/source/examples/00_misc/04_herten.py b/GSTools/source/examples/00_misc/04_herten.py new file mode 100644 index 0000000000000000000000000000000000000000..1e1b8a23f632f7cefa1f3da28b0a7126923de593 --- /dev/null +++ b/GSTools/source/examples/00_misc/04_herten.py @@ -0,0 +1,290 @@ +""" +Analyzing the Herten Aquifer with GSTools +----------------------------------------- + +This example is going to be a bit more extensive and we are going to do some +basic data preprocessing for the actual variogram estimation. But this example +will be self-contained and all data gathering and processing will be done in +this example script. + + +The Data +^^^^^^^^ + +We are going to analyse the Herten aquifer, which is situated in Southern +Germany. Multiple outcrop faces where surveyed and interpolated to a 3D +dataset. In these publications, you can find more information about the data: + +| Bayer, Peter; Comunian, Alessandro; Höyng, Dominik; Mariethoz, Gregoire (2015): Physicochemical properties and 3D geostatistical simulations of the Herten and the Descalvado aquifer analogs. PANGAEA, https://doi.org/10.1594/PANGAEA.844167, +| Supplement to: Bayer, P et al. (2015): Three-dimensional multi-facies realizations of sedimentary reservoir and aquifer analogs. Scientific Data, 2, 150033, https://doi.org/10.1038/sdata.2015.33 +| + +Retrieving the Data +^^^^^^^^^^^^^^^^^^^ + +To begin with, we need to download and extract the data. Therefore, we are +going to use some built-in Python libraries. For simplicity, many values and +strings will be hardcoded. + +You don't have to execute the ``download_herten`` and ``generate_transmissivity`` +functions, since the only produce the ``herten_transmissivity.gz`` +and ``grid_dim_origin_spacing.txt``, which are already present. +""" + +import os + +import matplotlib.pyplot as plt +import numpy as np + +import gstools as gs + +VTK_PATH = os.path.join("Herten-analog", "sim-big_1000x1000x140", "sim.vtk") + +############################################################################### + + +def download_herten(): + """Download the data, warning: its about 250MB.""" + import urllib.request + import zipfile + + print("Downloading Herten data") + data_filename = "data.zip" + data_url = ( + "http://store.pangaea.de/Publications/" + "Bayer_et_al_2015/Herten-analog.zip" + ) + urllib.request.urlretrieve(data_url, "data.zip") + # extract the "big" simulation + with zipfile.ZipFile(data_filename, "r") as zf: + zf.extract(VTK_PATH) + + +############################################################################### + + +def generate_transmissivity(): + """Generate a file with a transmissivity field from the HERTEN data.""" + import shutil + + import pyvista as pv + + print("Loading Herten data with pyvista") + mesh = pv.read(VTK_PATH) + herten = mesh.point_data["facies"].reshape(mesh.dimensions, order="F") + # conductivity values per fazies from the supplementary data + cond = 1e-4 * np.array( + [2.5, 2.3, 0.61, 260, 1300, 950, 0.43, 0.006, 23, 1.4] + ) + # asign the conductivities to the facies + herten_cond = cond[herten] + # Next, we are going to calculate the transmissivity, + # by integrating over the vertical axis + herten_trans = np.sum(herten_cond, axis=2) * mesh.spacing[2] + # saving some grid informations + grid = [mesh.dimensions[:2], mesh.origin[:2], mesh.spacing[:2]] + print("Saving the transmissivity field and grid information") + np.savetxt("herten_transmissivity.gz", herten_trans) + np.savetxt("grid_dim_origin_spacing.txt", grid) + # Some cleanup. You can comment out these lines to keep the downloaded data + os.remove("data.zip") + shutil.rmtree("Herten-analog") + + +############################################################################### +# Downloading and Preprocessing +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +# +# You can uncomment the following two calls, so the data is downloaded +# and processed again. + +# download_herten() +# generate_transmissivity() + + +############################################################################### +# Analyzing the data +# ^^^^^^^^^^^^^^^^^^ +# +# The Herten data provides information about the grid, which was already used in +# the previous code block. From this information, we can create our own grid on +# which we can estimate the variogram. As a first step, we are going to estimate +# an isotropic variogram, meaning that we will take point pairs from all +# directions into account. An unstructured grid is a natural choice for this. +# Therefore, we are going to create an unstructured grid from the given, +# structured one. For this, we are going to write another small function + +herten_log_trans = np.log(np.loadtxt("herten_transmissivity.gz")) +dim, origin, spacing = np.loadtxt("grid_dim_origin_spacing.txt") + +# create a structured grid on which the data is defined +x_s = np.arange(origin[0], origin[0] + dim[0] * spacing[0], spacing[0]) +y_s = np.arange(origin[1], origin[1] + dim[1] * spacing[1], spacing[1]) +# create the corresponding unstructured grid for the variogram estimation +x_u, y_u = np.meshgrid(x_s, y_s) + + +############################################################################### +# Let's have a look at the transmissivity field of the Herten aquifer + +plt.imshow(herten_log_trans.T, origin="lower", aspect="equal") +plt.show() + + +############################################################################### +# Estimating the Variogram +# ^^^^^^^^^^^^^^^^^^^^^^^^ +# +# Finally, everything is ready for the variogram estimation. For the unstructured +# method, we have to define the bins on which the variogram will be estimated. +# Through expert knowledge (i.e. fiddling around), we assume that the main +# features of the variogram will be below 10 metres distance. And because the +# data has a high spatial resolution, the resolution of the bins can also be +# high. The transmissivity data is still defined on a structured grid, but we can +# simply flatten it with :any:`numpy.ndarray.flatten`, in order to bring it into +# the right shape. It might be more memory efficient to use +# ``herten_log_trans.reshape(-1)``, but for better readability, we will stick to +# :any:`numpy.ndarray.flatten`. Taking all data points into account would take a +# very long time (expert knowledge \*wink\*), thus we will only take 2000 datapoints into account, which are sampled randomly. In order to make the exact +# results reproducible, we can also set a seed. + + +bins = gs.standard_bins(pos=(x_u, y_u), max_dist=10) +bin_center, gamma = gs.vario_estimate( + (x_u, y_u), + herten_log_trans.reshape(-1), + bins, + sampling_size=2000, + sampling_seed=19920516, +) + +############################################################################### +# The estimated variogram is calculated on the centre of the given bins, +# therefore, the ``bin_center`` array is also returned. + +############################################################################### +# Fitting the Variogram +# ^^^^^^^^^^^^^^^^^^^^^ +# +# Now, we can see, if the estimated variogram can be modelled by a common +# variogram model. Let's try the :any:`Exponential` model. + +# fit an exponential model +fit_model = gs.Exponential(dim=2) +fit_model.fit_variogram(bin_center, gamma, nugget=False) + +############################################################################### +# Finally, we can visualise some results. For quickly plotting a covariance +# model, GSTools provides some helper functions. + +ax = fit_model.plot(x_max=max(bin_center)) +ax.plot(bin_center, gamma) + + +############################################################################### +# That looks like a pretty good fit! By printing the model, we can directly see +# the fitted parameters + +print(fit_model) + +############################################################################### +# With this data, we could start generating new ensembles of the Herten aquifer +# with the :any:`SRF` class. + + +############################################################################### +# Estimating the Variogram in Specific Directions +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +# +# Estimating a variogram on a structured grid gives us the possibility to only +# consider values in a specific direction. This could be a first test, to see if +# the data is anisotropic. +# In order to speed up the calculations, we are going to only use every 10th datapoint and for a comparison with the isotropic variogram calculated earlier, we +# only need the first 21 array items. + + +# estimate the variogram on a structured grid +# use only every 10th value, otherwise calculations would take very long +x_s_skip = np.ravel(x_s)[::10] +y_s_skip = np.ravel(y_s)[::10] +herten_trans_skip = herten_log_trans[::10, ::10] + +############################################################################### +# With this much smaller data set, we can immediately estimate the variogram in +# the x- and y-axis + +gamma_x = gs.vario_estimate_axis(herten_trans_skip, direction="x") +gamma_y = gs.vario_estimate_axis(herten_trans_skip, direction="y") + +############################################################################### +# With these two estimated variograms, we can start fitting :any:`Exponential` +# covariance models + +x_plot = x_s_skip[:21] +y_plot = y_s_skip[:21] +# fit an exponential model +fit_model_x = gs.Exponential(dim=2) +fit_model_x.fit_variogram(x_plot, gamma_x[:21], nugget=False) +fit_model_y = gs.Exponential(dim=2) +fit_model_y.fit_variogram(y_plot, gamma_y[:21], nugget=False) + +############################################################################### +# Now, the isotropic variogram and the two variograms in x- and y-direction can +# be plotted together with their respective models, which will be plotted with +# dashed lines. + +plt.figure() # new figure +(line,) = plt.plot(bin_center, gamma, label="estimated variogram (isotropic)") +plt.plot( + bin_center, + fit_model.variogram(bin_center), + color=line.get_color(), + linestyle="--", + label="exp. variogram (isotropic)", +) + +(line,) = plt.plot(x_plot, gamma_x[:21], label="estimated variogram in x-dir") +plt.plot( + x_plot, + fit_model_x.variogram(x_plot), + color=line.get_color(), + linestyle="--", + label="exp. variogram in x-dir", +) + +(line,) = plt.plot(y_plot, gamma_y[:21], label="estimated variogram in y-dir") +plt.plot( + y_plot, + fit_model_y.variogram(y_plot), + color=line.get_color(), + linestyle="--", + label="exp. variogram in y-dir", +) + +plt.legend() +plt.show() + +############################################################################### +# The plot might be a bit cluttered, but at least it is pretty obvious that the +# Herten aquifer has no apparent anisotropies in its spatial structure. + +print("semivariogram model (isotropic):\n", fit_model) +print("semivariogram model (in x-dir.):\n", fit_model_x) +print("semivariogram model (in y-dir.):\n", fit_model_y) + + +############################################################################### +# Creating a Spatial Random Field from the Herten Parameters +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +# +# With all the hard work done, it's straight forward now, to generate new +# *Herten-like realisations* + +# create a spatial random field on the low-resolution grid +srf = gs.SRF(fit_model, seed=19770928) +srf.structured([x_s_skip, y_s_skip]) +ax = srf.plot() +ax.set_aspect("equal") + +############################################################################### +# That's pretty neat! diff --git a/GSTools/source/examples/00_misc/05_standalone_field.py b/GSTools/source/examples/00_misc/05_standalone_field.py new file mode 100644 index 0000000000000000000000000000000000000000..e467f043196a77917f7a1b980809b05583b2c194 --- /dev/null +++ b/GSTools/source/examples/00_misc/05_standalone_field.py @@ -0,0 +1,30 @@ +""" +Standalone Field class +---------------------- + +The :any:`Field` class of GSTools can be used to plot arbitrary data in nD. + +In the following example we will produce 10000 random points in 4D with +random values and plot them. +""" + +import numpy as np + +import gstools as gs + +rng = np.random.RandomState(19970221) +x0 = rng.rand(10000) * 100.0 +x1 = rng.rand(10000) * 100.0 +x2 = rng.rand(10000) * 100.0 +x3 = rng.rand(10000) * 100.0 +values = rng.rand(10000) * 100.0 + +############################################################################### +# Only thing needed to instantiate the Field is the dimension. +# +# Afterwards we can call the instance like all other Fields +# (:any:`SRF`, :any:`Krige` or :any:`CondSRF`), but with an additional field. + +plotter = gs.field.Field(dim=4) +plotter(pos=(x0, x1, x2, x3), field=values) +plotter.plot() diff --git a/GSTools/source/examples/00_misc/README.rst b/GSTools/source/examples/00_misc/README.rst new file mode 100644 index 0000000000000000000000000000000000000000..bef7ae5726e910b6d4737301a04797d0edf3894d --- /dev/null +++ b/GSTools/source/examples/00_misc/README.rst @@ -0,0 +1,9 @@ +Miscellaneous Tutorials +======================= + +More examples which do not really fit into other categories. Some are not more +than a code snippet, while others are more complex and more than one part of +GSTools is involved. + +Examples +-------- diff --git a/GSTools/source/examples/00_misc/grid_dim_origin_spacing.txt b/GSTools/source/examples/00_misc/grid_dim_origin_spacing.txt new file mode 100644 index 0000000000000000000000000000000000000000..024928d3e3482c8ca8168ceb9b056ecdbd6e664a --- /dev/null +++ b/GSTools/source/examples/00_misc/grid_dim_origin_spacing.txt @@ -0,0 +1,3 @@ +1.000000000000000000e+03 1.000000000000000000e+03 +0.000000000000000000e+00 0.000000000000000000e+00 +5.000000000000000278e-02 5.000000000000000278e-02 diff --git a/GSTools/source/examples/00_misc/herten_transmissivity.gz b/GSTools/source/examples/00_misc/herten_transmissivity.gz new file mode 100644 index 0000000000000000000000000000000000000000..abc4d0239016721f1c87c67183ad18a6aa1f1b28 --- /dev/null +++ b/GSTools/source/examples/00_misc/herten_transmissivity.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:51216d681de300922eb2eecefe19abcb1256e84f8b2d3f86d09648c9c87a9dd2 +size 7274190 diff --git a/GSTools/source/examples/01_random_field/00_gaussian.py b/GSTools/source/examples/01_random_field/00_gaussian.py new file mode 100644 index 0000000000000000000000000000000000000000..b7bde5f9c9e4e74c1af2b0e0bfe59b87ce99f6bc --- /dev/null +++ b/GSTools/source/examples/01_random_field/00_gaussian.py @@ -0,0 +1,38 @@ +r""" +A Very Simple Example +--------------------- + +We are going to start with a very simple example of a spatial random field +with an isotropic Gaussian covariance model and following parameters: + +- variance :math:`\sigma^2=1` +- correlation length :math:`\lambda=10` + +First, we set things up and create the axes for the field. We are going to +need the :any:`SRF` class for the actual generation of the spatial random field. +But :any:`SRF` also needs a covariance model and we will simply take the +:any:`Gaussian` model. +""" + +import gstools as gs + +x = y = range(100) + +############################################################################### +# Now we create the covariance model with the parameters :math:`\sigma^2` and +# :math:`\lambda` and hand it over to :any:`SRF`. By specifying a seed, +# we make sure to create reproducible results: + +model = gs.Gaussian(dim=2, var=1, len_scale=10) +srf = gs.SRF(model, seed=20170519) + +############################################################################### +# With these simple steps, everything is ready to create our first random field. +# We will create the field on a structured grid (as you might have guessed from +# the `x` and `y`), which makes it easier to plot. + +field = srf.structured([x, y]) +srf.plot() + +############################################################################### +# Wow, that was pretty easy! diff --git a/GSTools/source/examples/01_random_field/01_srf_ensemble.py b/GSTools/source/examples/01_random_field/01_srf_ensemble.py new file mode 100644 index 0000000000000000000000000000000000000000..68cc2c0fa37a828c4d153a373a512b1544160721 --- /dev/null +++ b/GSTools/source/examples/01_random_field/01_srf_ensemble.py @@ -0,0 +1,54 @@ +""" +Creating an Ensemble of Fields +------------------------------ + +Creating an ensemble of random fields would also be +a great idea. Let's reuse most of the previous code. + +We will set the position tuple `pos` before generation to reuse it afterwards. +""" + +import matplotlib.pyplot as pt +import numpy as np + +import gstools as gs + +x = y = np.arange(100) + +model = gs.Gaussian(dim=2, var=1, len_scale=10) +srf = gs.SRF(model) +srf.set_pos([x, y], "structured") + +############################################################################### +# This time, we did not provide a seed to :any:`SRF`, as the seeds will used +# during the actual computation of the fields. We will create four ensemble +# members, for better visualisation, save them in to srf class and in a first +# step, we will be using the loop counter as the seeds. + +ens_no = 4 +for i in range(ens_no): + srf(seed=i, store=f"field{i}") + +############################################################################### +# Now let's have a look at the results. We can access the fields by name or +# index: + +fig, ax = pt.subplots(2, 2, sharex=True, sharey=True) +ax = ax.flatten() +for i in range(ens_no): + ax[i].imshow(srf[i].T, origin="lower") +pt.show() + +############################################################################### +# Using better Seeds +# ^^^^^^^^^^^^^^^^^^ +# +# It is not always a good idea to use incrementing seeds. Therefore GSTools +# provides a seed generator :any:`MasterRNG`. The loop, in which the fields are +# generated would then look like + +from gstools.random import MasterRNG + +seed = MasterRNG(20170519) +for i in range(ens_no): + srf(seed=seed(), store=f"better_field{i}") diff --git a/GSTools/source/examples/01_random_field/02_fancier.py b/GSTools/source/examples/01_random_field/02_fancier.py new file mode 100644 index 0000000000000000000000000000000000000000..1922cec7284db0dea18fe69d633c5a366027bf71 --- /dev/null +++ b/GSTools/source/examples/01_random_field/02_fancier.py @@ -0,0 +1,31 @@ +r""" +Creating Fancier Fields +----------------------- + +Only using Gaussian covariance fields gets boring. Now we are going to create +much rougher random fields by using an exponential covariance model and we are going to make them anisotropic. + +The code is very similar to the previous examples, but with a different +covariance model class :any:`Exponential`. As model parameters we a using +following + +- variance :math:`\sigma^2=1` +- correlation length :math:`\lambda=(12, 3)^T` +- rotation angle :math:`\theta=\pi/8` + +""" + +import numpy as np + +import gstools as gs + +x = y = np.arange(100) +model = gs.Exponential(dim=2, var=1, len_scale=[12.0, 3.0], angles=np.pi / 8) +srf = gs.SRF(model, seed=20170519) +srf.structured([x, y]) +srf.plot() + +############################################################################### +# The anisotropy ratio could also have been set with + +model = gs.Exponential(dim=2, var=1, len_scale=12, anis=0.25, angles=np.pi / 8) diff --git a/GSTools/source/examples/01_random_field/03_unstr_srf_export.py b/GSTools/source/examples/01_random_field/03_unstr_srf_export.py new file mode 100644 index 0000000000000000000000000000000000000000..94d00952541d6eb8ec775725745ebbe4beadb815 --- /dev/null +++ b/GSTools/source/examples/01_random_field/03_unstr_srf_export.py @@ -0,0 +1,34 @@ +""" +Using an Unstructured Grid +-------------------------- + +For many applications, the random fields are needed on an unstructured grid. +Normally, such a grid would be read in, but we can simply generate one and +then create a random field at those coordinates. +""" + +import numpy as np + +import gstools as gs + +############################################################################### +# Creating our own unstructured grid +seed = gs.random.MasterRNG(19970221) +rng = np.random.RandomState(seed()) +x = rng.randint(0, 100, size=10000) +y = rng.randint(0, 100, size=10000) + +model = gs.Exponential(dim=2, var=1, len_scale=[12, 3], angles=np.pi / 8) +srf = gs.SRF(model, seed=20170519) +field = srf((x, y)) +srf.vtk_export("field") +# Or create a PyVista dataset +# mesh = srf.to_pyvista() + +############################################################################### +ax = srf.plot() +ax.set_aspect("equal") + +############################################################################### +# Comparing this image to the previous one, you can see that be using the same +# seed, the same field can be computed on different grids. diff --git a/GSTools/source/examples/01_random_field/04_srf_merge.py b/GSTools/source/examples/01_random_field/04_srf_merge.py new file mode 100644 index 0000000000000000000000000000000000000000..241ed0793fedf87d5980efafce16afc808277886 --- /dev/null +++ b/GSTools/source/examples/01_random_field/04_srf_merge.py @@ -0,0 +1,49 @@ +""" +Merging two Fields +------------------ + +We can even generate the same field realisation on different grids. Let's try +to merge two unstructured rectangular fields. + +""" + +# sphinx_gallery_thumbnail_number = 2 +import numpy as np + +import gstools as gs + +# creating our own unstructured grid +seed = gs.random.MasterRNG(19970221) +rng = np.random.RandomState(seed()) +x = rng.randint(0, 100, size=10000) +y = rng.randint(0, 100, size=10000) + +model = gs.Exponential(dim=2, var=1, len_scale=[12, 3], angles=np.pi / 8) +srf = gs.SRF(model, seed=20170519) +field1 = srf((x, y)) +srf.plot() +############################################################################### +# But now we extend the field on the right hand side by creating a new +# unstructured grid and calculating a field with the same parameters and the +# same seed on it: + +# new grid +seed = gs.random.MasterRNG(20011012) +rng = np.random.RandomState(seed()) +x2 = rng.randint(99, 150, size=10000) +y2 = rng.randint(20, 80, size=10000) + +field2 = srf((x2, y2)) +ax = srf.plot() +ax.tricontourf(x, y, field1.T, levels=256) +ax.set_aspect("equal") + +############################################################################### +# The slight mismatch where the two fields were merged is merely due to +# interpolation problems of the plotting routine. You can convince yourself +# be increasing the resolution of the grids by a factor of 10. +# +# Of course, this merging could also have been done by appending the grid +# point ``(x2, y2)`` to the original grid ``(x, y)`` before generating the field. +# But one application scenario would be to generate hugh fields, which would not +# fit into memory anymore. diff --git a/GSTools/source/examples/01_random_field/05_mesh_ensemble.py b/GSTools/source/examples/01_random_field/05_mesh_ensemble.py new file mode 100644 index 0000000000000000000000000000000000000000..1e2f0da86c5d2270d0ba1c1ea71997c18d2f58dc --- /dev/null +++ b/GSTools/source/examples/01_random_field/05_mesh_ensemble.py @@ -0,0 +1,94 @@ +""" +Generating Fields on Meshes +--------------------------- + +GSTools provides an interface for meshes, to support +`meshio `_ and +`ogs5py `_ meshes. + +When using `meshio`, the generated fields will be stored immediately in the +mesh container. + +There are two options to generate a field on a given mesh: + +- `points="points"` will generate a field on the mesh points +- `points="centroids"` will generate a field on the cell centroids + +In this example, we will generate a simple mesh with the aid of +`meshzoo `_. +""" + +import matplotlib.pyplot as plt +import matplotlib.tri as tri +import meshio +import meshzoo +import numpy as np + +import gstools as gs + +# generate a triangulated hexagon with meshzoo +points, cells = meshzoo.ngon(6, 4) +mesh = meshio.Mesh(points, {"triangle": cells}) + +############################################################################### +# Now we prepare the SRF class as always. We will generate an ensemble of +# fields on the generated mesh. + +# number of fields +fields_no = 12 +# model setup +model = gs.Gaussian(dim=2, len_scale=0.5) +srf = gs.SRF(model, mean=1) + +############################################################################### +# To generate fields on a mesh, we provide a separate method: :any:`SRF.mesh`. +# First we generate fields on the mesh-centroids controlled by a seed. +# You can specify the field name by the keyword `name`. + +for i in range(fields_no): + srf.mesh(mesh, points="centroids", name=f"c-field-{i}", seed=i) + +############################################################################### +# Now we generate fields on the mesh-points again controlled by a seed. + +for i in range(fields_no): + srf.mesh(mesh, points="points", name=f"p-field-{i}", seed=i) + +############################################################################### +# To get an impression we now want to plot the generated fields. +# Luckily, matplotlib supports triangular meshes. + +triangulation = tri.Triangulation(points[:, 0], points[:, 1], cells) +# figure setup +cols = 4 +rows = int(np.ceil(fields_no / cols)) + +############################################################################### +# Cell data can be easily visualized with matplotlibs `tripcolor`. +# To highlight the cell structure, we use `triplot`. + +fig = plt.figure(figsize=[2 * cols, 2 * rows]) +for i, field in enumerate(mesh.cell_data, 1): + ax = fig.add_subplot(rows, cols, i) + ax.tripcolor(triangulation, mesh.cell_data[field][0]) + ax.triplot(triangulation, linewidth=0.5, color="k") + ax.set_aspect("equal") +fig.tight_layout() + +############################################################################### +# Point data is plotted via `tricontourf`. + +fig = plt.figure(figsize=[2 * cols, 2 * rows]) +for i, field in enumerate(mesh.point_data, 1): + ax = fig.add_subplot(rows, cols, i) + ax.tricontourf(triangulation, mesh.point_data[field]) + ax.triplot(triangulation, linewidth=0.5, color="k") + ax.set_aspect("equal") +fig.tight_layout() +plt.show() + +############################################################################### +# Last but not least, `meshio` can be used for what is does best: Exporting. +# Tada! + +mesh.write("mesh_ensemble.vtk") diff --git a/GSTools/source/examples/01_random_field/06_pyvista_support.py b/GSTools/source/examples/01_random_field/06_pyvista_support.py new file mode 100644 index 0000000000000000000000000000000000000000..b560dfd8f7434acf191a3a45752b6597ac84b267 --- /dev/null +++ b/GSTools/source/examples/01_random_field/06_pyvista_support.py @@ -0,0 +1,58 @@ +""" +Using PyVista meshes +-------------------- + +`PyVista `__ is a helper module for the +Visualization Toolkit (VTK) that takes a different approach on interfacing with +VTK through NumPy and direct array access. + +It provides mesh data structures and filtering methods for spatial datasets, +makes 3D plotting simple and is built for large/complex data geometries. + +The :any:`Field.mesh` method enables easy field creation on PyVista meshes +used by the :any:`SRF` or :any:`Krige` class. +""" + +# sphinx_gallery_thumbnail_path = 'pics/GS_pyvista_cut.png' +import pyvista as pv + +import gstools as gs + +############################################################################### +# We create a structured grid with PyVista containing 50 segments on all three +# axes each with a length of 2 (whatever unit). + +dims, spacing = (50, 50, 50), (2, 2, 2) +grid = pv.ImageData(dimensions=dims, spacing=spacing) + +############################################################################### +# Now we set up the SRF class as always. We'll use an anisotropic model. + +model = gs.Gaussian(dim=3, len_scale=[16, 8, 4], angles=(0.8, 0.4, 0.2)) +srf = gs.SRF(model, seed=19970221) + +############################################################################### +# The PyVista mesh can now be directly passed to the :any:`SRF.mesh` method. +# When dealing with meshes, one can choose if the field should be generated +# on the mesh-points (`"points"`) or the cell-centroids (`"centroids"`). +# +# In addition we can set a name, under which the resulting field is stored +# in the mesh. + +srf.mesh(grid, points="points", name="random-field") + +############################################################################### +# Now we have access to PyVista's abundancy of methods to explore the field. +# +# .. note:: +# PyVista is not working on readthedocs, but you can try it out yourself by +# uncommenting the following line of code. + +# grid.contour(isosurfaces=8).plot() + +############################################################################### +# The result should look like this: +# +# .. image:: ../../pics/GS_pyvista_cut.png +# :width: 400px +# :align: center diff --git a/GSTools/source/examples/01_random_field/07_higher_dimensions.py b/GSTools/source/examples/01_random_field/07_higher_dimensions.py new file mode 100644 index 0000000000000000000000000000000000000000..b65ab3ead93d731c5927f3584cf7757a5cbdd563 --- /dev/null +++ b/GSTools/source/examples/01_random_field/07_higher_dimensions.py @@ -0,0 +1,82 @@ +""" +Higher Dimensions +----------------- + +GSTools provides experimental support for higher dimensions. + +Anisotropy is the same as in lower dimensions: + +- in `n` dimensions we need `(n-1)` anisotropy ratios + +Rotation on the other hand is a bit more complex. +With increasing dimensions more and more rotation angles are added in order +to properply describe the rotated axes of anisotropy. + +By design the first rotation angles coincide with the lower ones: + +- 2D (rotation in x-y plane) -> 3D: first angle describes xy-plane rotation +- 3D (Tait-Bryan angles) -> 4D: first 3 angles coincide with Tait-Bryan angles + +By increasing the dimension from `n` to `(n+1)`, `n` angles are added: + +- 2D (1 angle) -> 3D: 3 angles (2 added) +- 3D (3 angles) -> 4D: 6 angles (3 added) + +the following list of rotation-planes are described by the list of +angles in the model: + +1. x-y plane +2. x-z plane +3. y-z plane +4. x-v plane +5. y-v plane +6. z-v plane +7. ... + +The rotation direction in these planes have alternating signs +in order to match Tait-Bryan in 3D. + +Let's have a look at a 4D example, where we naively add a 4th dimension. +""" + +import matplotlib.pyplot as plt + +import gstools as gs + +dim = 4 +size = 20 +pos = [range(size)] * dim +model = gs.Exponential(dim=dim, len_scale=5) +srf = gs.SRF(model, seed=20170519) +field = srf.structured(pos) + +############################################################################### +# In order to "prove" correctness, we can calculate an empirical variogram +# of the generated field and fit our model to it. + +bin_center, vario = gs.vario_estimate( + pos, field, sampling_size=2000, mesh_type="structured" +) +model.fit_variogram(bin_center, vario) +print(model) + +############################################################################### +# As you can see, the estimated variance and length scale match our input +# quite well. +# +# Let's have a look at the fit and a x-y cross-section of the 4D field: + +f, a = plt.subplots(1, 2, gridspec_kw={"width_ratios": [2, 1]}, figsize=[9, 3]) +model.plot(x_max=max(bin_center), ax=a[0]) +a[0].scatter(bin_center, vario) +a[1].imshow(field[:, :, 0, 0].T, origin="lower") +a[0].set_title("isotropic empirical variogram with fitted model") +a[1].set_title("x-y cross-section") +f.show() + +############################################################################### +# GSTools also provides plotting routines for higher dimensions. +# Fields are shown by 2D cross-sections, where other dimensions can be +# controlled via sliders. + +srf.plot() diff --git a/GSTools/source/examples/01_random_field/08_fourier.py b/GSTools/source/examples/01_random_field/08_fourier.py new file mode 100644 index 0000000000000000000000000000000000000000..8b700ae6e383f43ac9e902c736ae2943ac2bd85a --- /dev/null +++ b/GSTools/source/examples/01_random_field/08_fourier.py @@ -0,0 +1,44 @@ +""" +Generating a Simple Periodic Random Field +----------------------------------------- + +In this simple example we are going to learn how to generate periodic spatial +random fields. The Fourier method comes naturally with the property of +periodicity, so we'll use it to create the random field. +""" + +import numpy as np + +import gstools as gs + +# We start off by defining the spatial grid. For the sake of simplicity, we +# use a square domain. We set the optional argument `endpoint` to `False`, to +# not make the domain in each dimension one grid cell larger than the +# periodicity. +L = 500.0 +x = np.linspace(0, L, 256, endpoint=False) +y = np.linspace(0, L, 128, endpoint=False) + +# Now, we create a Gaussian covariance model with a correlation length which is +# roughly half the size of the grid. +model = gs.Gaussian(dim=2, var=1, len_scale=200) + +# Next, we hand the cov. model to the spatial random field class `SRF` +# and set the generator to `"Fourier"`. The argument `period` is set to the +# domain size. If only a single number is given, the same periodicity is +# applied in each dimension, as shown in this example. The `mode_no` argument +# sets the number of Fourier modes. If only an integer is given, that number +# of modes is used for all dimensions. +srf = gs.SRF( + model, + generator="Fourier", + period=L, + mode_no=32, + seed=1681903, +) + +# Now, we can calculate the field with the given parameters. +srf((x, y), mesh_type="structured") + +# GSTools has a few simple visualization methods built in. +srf.plot() diff --git a/GSTools/source/examples/01_random_field/09_fourier_trans.py b/GSTools/source/examples/01_random_field/09_fourier_trans.py new file mode 100644 index 0000000000000000000000000000000000000000..81c303f3987a0016e577f8cd2ab0aa8c529cbfa6 --- /dev/null +++ b/GSTools/source/examples/01_random_field/09_fourier_trans.py @@ -0,0 +1,49 @@ +""" +Generating a Transformed Periodic Random Field +---------------------------------------------- + +Building on the precious example, we are now going to generate periodic +spatial random fields with a transformation applied, resulting in a level set. +""" + +import numpy as np + +import gstools as gs + +# We start off by defining the spatial grid. As in the previous example, we do +# not want to include the endpoints. +L = np.array((500, 400)) +x = np.linspace(0, L[0], 300, endpoint=False) +y = np.linspace(0, L[1], 200, endpoint=False) + +# Instead of using a Gaussian covariance model, we will use the much rougher +# exponential model and we will introduce an anisotropy by using two different +# length scales in the x- and y-directions +model = gs.Exponential(dim=2, var=2, len_scale=[80, 20]) + +# Same as before, we set up the spatial random field. But this time, we will +# use a periodicity which is equal to the domain size in x-direction, but +# half the domain size in y-direction. And we will use different `mode_no` for +# the different dimensions. +srf = gs.SRF( + model, + generator="Fourier", + period=[L[0], L[1] / 2], + mode_no=[30, 20], + seed=1681903, +) +# and compute it on our spatial domain +srf((x, y), mesh_type="structured") + +# With the field generated, we can now apply transformations starting with a +# discretization of the field into 4 different values +thresholds = np.linspace(np.min(srf.field), np.max(srf.field), 4) +srf.transform("discrete", store="transform_discrete", values=thresholds) +srf.plot("transform_discrete") + +# This is already a nice result, but we want to pronounce the peaks of the +# field. We can do this by applying a log-normal transformation on top +srf.transform( + "lognormal", field="transform_discrete", store="transform_lognormal" +) +srf.plot("transform_lognormal") diff --git a/GSTools/source/examples/01_random_field/README.rst b/GSTools/source/examples/01_random_field/README.rst new file mode 100644 index 0000000000000000000000000000000000000000..ab723bae35927b81f96f9524d1b14ea50b4b85ee --- /dev/null +++ b/GSTools/source/examples/01_random_field/README.rst @@ -0,0 +1,23 @@ +Random Field Generation +======================= + +The main feature of GSTools is the spatial random field generator :any:`SRF`, +which can generate random fields following a given covariance model. +The generator provides a lot of nice features, which will be explained in +the following + +GSTools generates spatial random fields with a given covariance model or +semi-variogram. This is done by using the so-called randomization method. +The spatial random field is represented by a stochastic Fourier integral +and its discretised modes are evaluated at random frequencies. + +In case you want to generate spatial random fields with periodic boundaries, +you can use the so-called Fourier method. See the corresponding examples for +how to do that. The spatial random field is represented by a stochastic +Fourier integral and its discretised modes are evaluated at equidistant +frequencies. + +GSTools supports arbitrary and non-isotropic covariance models. + +Examples +-------- diff --git a/GSTools/source/examples/02_cov_model/00_intro.py b/GSTools/source/examples/02_cov_model/00_intro.py new file mode 100644 index 0000000000000000000000000000000000000000..6b73fd2a1ea228389e4e057732da5c90a90160fa --- /dev/null +++ b/GSTools/source/examples/02_cov_model/00_intro.py @@ -0,0 +1,75 @@ +""" +Introductory example +==================== + +Let us start with a short example of a self defined model (Of course, we +provide a lot of predefined models [See: :any:`gstools.covmodel`], +but they all work the same way). +Therefore we reimplement the Gaussian covariance model +by defining just the "normalized" +`correlation `_ +function: +""" + +import numpy as np + +import gstools as gs + + +# use CovModel as the base-class +class Gau(gs.CovModel): + def cor(self, h): + return np.exp(-(h**2)) + + +############################################################################### +# Here the parameter ``h`` stands for the normalized range ``r / len_scale``. +# Now we can instantiate this model: + +model = Gau(dim=2, var=2.0, len_scale=10) + +############################################################################### +# To have a look at the variogram, let's plot it: + +model.plot() + +############################################################################### +# This is almost identical to the already provided :any:`Gaussian` model. +# There, a scaling factor is implemented so the len_scale coincides with the +# integral scale: + +gau_model = gs.Gaussian(dim=2, var=2.0, len_scale=10) +gau_model.plot() + + +############################################################################### +# Parameters +# ---------- +# +# We already used some parameters, which every covariance models has. +# The basic ones are: +# +# - **dim** : dimension of the model +# - **var** : variance of the model (on top of the subscale variance) +# - **len_scale** : length scale of the model +# - **nugget** : nugget (subscale variance) of the model +# +# These are the common parameters used to characterize +# a covariance model and are therefore used by every model in GSTools. +# You can also access and reset them: + +print("old model:", model) +model.dim = 3 +model.var = 1 +model.len_scale = 15 +model.nugget = 0.1 +print("new model:", model) + + +############################################################################### +# .. note:: +# +# - The sill of the variogram is calculated by ``sill = variance + nugget`` +# So we treat the variance as everything **above** the nugget, +# which is sometimes called **partial sill**. +# - A covariance model can also have additional parameters. diff --git a/GSTools/source/examples/02_cov_model/01_basic_methods.py b/GSTools/source/examples/02_cov_model/01_basic_methods.py new file mode 100644 index 0000000000000000000000000000000000000000..4c97fba85c0e8f0b6986d3e78801499e9861b11a --- /dev/null +++ b/GSTools/source/examples/02_cov_model/01_basic_methods.py @@ -0,0 +1,45 @@ +r""" +Basic Methods +============= + +The covariance model class :any:`CovModel` of GSTools provides a set of handy +methods. + +One of the following functions defines the main characterization of the +variogram: + +- ``CovModel.variogram`` : The variogram of the model given by + + .. math:: + \gamma\left(r\right)= + \sigma^2\cdot\left(1-\rho\left(r\right)\right)+n + +- ``CovModel.covariance`` : The (auto-)covariance of the model given by + + .. math:: + C\left(r\right)= \sigma^2\cdot\rho\left(r\right) + +- ``CovModel.correlation`` : The (auto-)correlation + (or normalized covariance) of the model given by + + .. math:: + \rho\left(r\right) + +- ``CovModel.cor`` : The normalized correlation taking a + normalized range given by: + + .. math:: + \mathrm{cor}\left(\frac{r}{\ell}\right) = \rho\left(r\right) + + +As you can see, it is the easiest way to define a covariance model by giving a +correlation function as demonstrated in the introductory example. +If one of the above functions is given, the others will be determined: +""" + +import gstools as gs + +model = gs.Exponential(dim=3, var=2.0, len_scale=10, nugget=0.5) +ax = model.plot("variogram") +model.plot("covariance", ax=ax) +model.plot("correlation", ax=ax) diff --git a/GSTools/source/examples/02_cov_model/02_aniso_rotation.py b/GSTools/source/examples/02_cov_model/02_aniso_rotation.py new file mode 100644 index 0000000000000000000000000000000000000000..b7459e39611c7b2da938b950642ee754140e97da --- /dev/null +++ b/GSTools/source/examples/02_cov_model/02_aniso_rotation.py @@ -0,0 +1,56 @@ +""" +Anisotropy and Rotation +======================= + +The internally used (semi-) variogram +represents the isotropic case for the model. +Nevertheless, you can provide anisotropy ratios by: +""" + +import gstools as gs + +model = gs.Gaussian(dim=3, var=2.0, len_scale=10, anis=0.5) +print(model.anis) +print(model.len_scale_vec) + + +############################################################################### +# As you can see, we defined just one anisotropy-ratio +# and the second transversal direction was filled up with ``1.``. +# You can get the length-scales in each direction by +# the attribute :any:`CovModel.len_scale_vec`. For full control you can set +# a list of anistropy ratios: ``anis=[0.5, 0.4]``. +# +# Alternatively you can provide a list of length-scales: + +model = gs.Gaussian(dim=3, var=2.0, len_scale=[10, 5, 4]) +model.plot("vario_spatial") +print("Anisotropy representations:") +print("Anis. ratios:", model.anis) +print("Main length scale", model.len_scale) +print("All length scales", model.len_scale_vec) + + +############################################################################### +# Rotation Angles +# --------------- +# +# The main directions of the field don't have to coincide with the spatial +# directions :math:`x`, :math:`y` and :math:`z`. Therefore you can provide +# rotation angles for the model: + +model = gs.Gaussian(dim=3, var=2.0, len_scale=[10, 2], angles=2.5) +model.plot("vario_spatial") +print("Rotation angles", model.angles) + +############################################################################### +# Again, the angles were filled up with ``0.`` to match the dimension and you +# could also provide a list of angles. The number of angles depends on the +# given dimension: +# +# - in 1D: no rotation performable +# - in 2D: given as rotation around z-axis +# - in 3D: given by yaw, pitch, and roll (known as +# `Tait–Bryan `_ +# angles) +# - in nD: See the random field example about higher dimensions diff --git a/GSTools/source/examples/02_cov_model/03_spectral_methods.py b/GSTools/source/examples/02_cov_model/03_spectral_methods.py new file mode 100644 index 0000000000000000000000000000000000000000..61c7e49b0c91d2047c8d2bf49c2e7ac52c126eb2 --- /dev/null +++ b/GSTools/source/examples/02_cov_model/03_spectral_methods.py @@ -0,0 +1,47 @@ +r""" +Spectral methods +================ + +The spectrum of a covariance model is given by: + +.. math:: S(\mathbf{k}) = \left(\frac{1}{2\pi}\right)^n + \int C(\Vert\mathbf{r}\Vert) e^{i b\mathbf{k}\cdot\mathbf{r}} d^n\mathbf{r} + +Since the covariance function :math:`C(r)` is radially symmetric, we can +calculate this by the +`hankel-transformation `_: + +.. math:: S(k) = \left(\frac{1}{2\pi}\right)^n \cdot + \frac{(2\pi)^{n/2}}{(bk)^{n/2-1}} + \int_0^\infty r^{n/2-1} C(r) J_{n/2-1}(bkr) r dr + +Where :math:`k=\left\Vert\mathbf{k}\right\Vert`. + +Depending on the spectrum, the spectral-density is defined by: + +.. math:: \tilde{S}(k) = \frac{S(k)}{\sigma^2} + +You can access these methods by: +""" + +import gstools as gs + +model = gs.Gaussian(dim=3, var=2.0, len_scale=10) +ax = model.plot("spectrum") +model.plot("spectral_density", ax=ax) + +############################################################################### +# .. note:: +# The spectral-density is given by the radius of the input phase. But it is +# **not** a probability density function for the radius of the phase. +# To obtain the pdf for the phase-radius, you can use the methods +# :any:`CovModel.spectral_rad_pdf` +# or :any:`CovModel.ln_spectral_rad_pdf` for the logarithm. +# +# The user can also provide a cdf (cumulative distribution function) by +# defining a method called ``spectral_rad_cdf`` +# and/or a ppf (percent-point function) +# by ``spectral_rad_ppf``. +# +# The attributes :any:`CovModel.has_cdf` +# and :any:`CovModel.has_ppf` will check for that. diff --git a/GSTools/source/examples/02_cov_model/04_different_scales.py b/GSTools/source/examples/02_cov_model/04_different_scales.py new file mode 100644 index 0000000000000000000000000000000000000000..0e2e1991b777d1eb14831d1fe6a3e0ad43917edf --- /dev/null +++ b/GSTools/source/examples/02_cov_model/04_different_scales.py @@ -0,0 +1,69 @@ +r""" +Different scales +================ + +Besides the length-scale, there are many other ways of characterizing a certain +scale of a covariance model. We provide two common scales with the covariance +model. + +Integral scale +-------------- + +The `integral scale `_ +of a covariance model is calculated by: + +.. math:: I = \int_0^\infty \rho(r) dr + +You can access it by: +""" + +import gstools as gs + +model = gs.Stable(dim=3, var=2.0, len_scale=10) +print("Main integral scale:", model.integral_scale) +print("All integral scales:", model.integral_scale_vec) + + +############################################################################### +# You can also specify integral length scales like the ordinary length scale, +# and len_scale/anis will be recalculated: + +model = gs.Stable(dim=3, var=2.0, integral_scale=[10, 4, 2]) +print("Anisotropy ratios:", model.anis) +print("Main length scale:", model.len_scale) +print("All length scales:", model.len_scale_vec) +print("Main integral scale:", model.integral_scale) +print("All integral scales:", model.integral_scale_vec) + + +############################################################################### +# Percentile scale +# ---------------- +# +# Another scale characterizing the covariance model, is the percentile scale. +# It is the distance, where the normalized +# variogram reaches a certain percentage of its sill. + +model = gs.Stable(dim=3, var=2.0, len_scale=10) +per_scale = model.percentile_scale(0.9) +int_scale = model.integral_scale +len_scale = model.len_scale +print("90% Percentile scale:", per_scale) +print("Integral scale:", int_scale) +print("Length scale:", len_scale) + +############################################################################### +# .. note:: +# +# The nugget is neglected by the percentile scale. +# +# +# Comparison +# ---------- + +ax = model.plot() +ax.axhline(1.8, color="k", label=r"90% percentile") +ax.axvline(per_scale, color="k", linestyle="--", label=r"90% percentile scale") +ax.axvline(int_scale, color="k", linestyle="-.", label=r"integral scale") +ax.axvline(len_scale, color="k", linestyle=":", label=r"length scale") +ax.legend() diff --git a/GSTools/source/examples/02_cov_model/05_additional_para.py b/GSTools/source/examples/02_cov_model/05_additional_para.py new file mode 100644 index 0000000000000000000000000000000000000000..3264cec472a1c84b54166dd7e494f072d92d1fd3 --- /dev/null +++ b/GSTools/source/examples/02_cov_model/05_additional_para.py @@ -0,0 +1,47 @@ +r""" +Additional Parameters +===================== + +Let's pimp our self-defined model ``Gau`` from the introductory example +by setting the exponent as an additional parameter: + +.. math:: + \rho(r) := \exp\left(-\left(\frac{r}{\ell}\right)^{\alpha}\right) + +This leads to the so called **stable** covariance model and we can define it by +""" + +import numpy as np + +import gstools as gs + + +class Stab(gs.CovModel): + def default_opt_arg(self): + return {"alpha": 1.5} + + def cor(self, h): + return np.exp(-(h**self.alpha)) + + +############################################################################### +# As you can see, we override the method :any:`CovModel.default_opt_arg` +# to provide a standard value for the optional argument ``alpha``. +# We can access it in the correlation function by ``self.alpha`` +# +# Now we can instantiate this model by either setting alpha implicitly with +# the default value or explicitly: + +model1 = Stab(dim=2, var=2.0, len_scale=10) +model2 = Stab(dim=2, var=2.0, len_scale=10, alpha=0.5) +ax = model1.plot() +model2.plot(ax=ax) + +############################################################################### +# Apparently, the parameter alpha controls the slope of the variogram +# and consequently the roughness of a generated random field. +# +# .. note:: +# +# You don't have to override the :any:`CovModel.default_opt_arg`, +# but you will get a ValueError if you don't set it on creation. diff --git a/GSTools/source/examples/02_cov_model/06_fitting_para_ranges.py b/GSTools/source/examples/02_cov_model/06_fitting_para_ranges.py new file mode 100644 index 0000000000000000000000000000000000000000..7c8f083edb7d009296348a10e070610ba8c77613 --- /dev/null +++ b/GSTools/source/examples/02_cov_model/06_fitting_para_ranges.py @@ -0,0 +1,76 @@ +""" +Fitting variogram data +====================== + +The model class comes with a routine to fit the model-parameters to given +variogram data. In the following we will use the self defined stable model +from a previous example. +""" + +import numpy as np + +import gstools as gs + + +class Stab(gs.CovModel): + def default_opt_arg(self): + return {"alpha": 1.5} + + def cor(self, h): + return np.exp(-(h**self.alpha)) + + +# Exemplary variogram data (e.g. estimated from field observations) +bins = [1.0, 3.0, 5.0, 7.0, 9.0, 11.0] +est_vario = [0.2, 0.5, 0.6, 0.8, 0.8, 0.9] +# fitting model +model = Stab(dim=2) +# we have to provide boundaries for the parameters +model.set_arg_bounds(alpha=[0, 3]) +results, pcov = model.fit_variogram(bins, est_vario, nugget=False) +print("Results:", results) + +############################################################################### + +ax = model.plot() +ax.scatter(bins, est_vario, color="k", label="sample variogram") +ax.legend() + + +############################################################################### +# As you can see, we have to provide boundaries for the parameters. +# As a default, the following bounds are set: +# +# - additional parameters: ``[-np.inf, np.inf]`` +# - variance: ``[0.0, np.inf]`` +# - len_scale: ``[0.0, np.inf]`` +# - nugget: ``[0.0, np.inf]`` +# +# Also, you can deselect parameters from fitting, so their predefined values +# will be kept. In our case, we fixed a ``nugget`` of ``0.0``, which was set +# by default. You can deselect any standard or +# optional argument of the covariance model. +# The second return value ``pcov`` is the estimated covariance of ``popt`` from +# the used scipy routine :any:`scipy.optimize.curve_fit`. +# +# You can use the following methods to manipulate the used bounds: +# +# .. currentmodule:: gstools.covmodel +# +# .. autosummary:: +# CovModel.default_opt_arg_bounds +# CovModel.default_arg_bounds +# CovModel.set_arg_bounds +# CovModel.check_arg_bounds +# +# You can override the :any:`CovModel.default_opt_arg_bounds` +# to provide standard bounds for your additional parameters. +# +# To access the bounds you can use: +# +# .. autosummary:: +# CovModel.var_bounds +# CovModel.len_scale_bounds +# CovModel.nugget_bounds +# CovModel.opt_arg_bounds +# CovModel.arg_bounds diff --git a/GSTools/source/examples/02_cov_model/README.rst b/GSTools/source/examples/02_cov_model/README.rst new file mode 100644 index 0000000000000000000000000000000000000000..73704183c46715d005f91c67055e4d2127530132 --- /dev/null +++ b/GSTools/source/examples/02_cov_model/README.rst @@ -0,0 +1,88 @@ +.. _tutorial_02_cov: + +The Covariance Model +==================== + +One of the core-features of GSTools is the powerful :any:`CovModel` +class, which allows you to easily define arbitrary covariance models by +yourself. The resulting models provide a bunch of nice features to explore the +covariance models. + +A covariance model is used to characterize the +`semi-variogram `_, +denoted by :math:`\gamma`, of a spatial random field. +In GSTools, we use the following form for an isotropic and stationary field: + +.. math:: + \gamma\left(r\right)= + \sigma^2\cdot\left(1-\mathrm{cor}\left(s\cdot\frac{r}{\ell}\right)\right)+n + +Where: + + - :math:`r` is the lag distance + - :math:`\ell` is the main correlation length + - :math:`s` is a scaling factor for unit conversion or normalization + - :math:`\sigma^2` is the variance + - :math:`n` is the nugget (subscale variance) + - :math:`\mathrm{cor}(h)` is the normalized correlation function depending on + the non-dimensional distance :math:`h=s\cdot\frac{r}{\ell}` + +Depending on the normalized correlation function, all covariance models in +GSTools are providing the following functions: + + - :math:`\rho(r)=\mathrm{cor}\left(s\cdot\frac{r}{\ell}\right)` + is the so called + `correlation `_ + function + - :math:`C(r)=\sigma^2\cdot\rho(r)` is the so called + `covariance `_ + function, which gives the name for our GSTools class + +.. note:: + + We are not limited to isotropic models. GSTools supports anisotropy ratios + for length scales in orthogonal transversal directions like: + + - :math:`x_0` (main direction) + - :math:`x_1` (1. transversal direction) + - :math:`x_2` (2. transversal direction) + - ... + + These main directions can also be rotated. + Just have a look at the corresponding examples. + +Provided Covariance Models +-------------------------- + +.. currentmodule:: gstools.covmodel + +The following standard covariance models are provided by GSTools + +.. autosummary:: + Gaussian + Exponential + Matern + Integral + Stable + Rational + Cubic + Linear + Circular + Spherical + HyperSpherical + SuperSpherical + JBessel + TPLSimple + +As a special feature, we also provide truncated power law (TPL) covariance models + +.. autosummary:: + TPLGaussian + TPLExponential + TPLStable + +These models provide a lower and upper length scale truncation +for superpositioned models. + +Examples +-------- diff --git a/GSTools/source/examples/03_variogram/00_fit_variogram.py b/GSTools/source/examples/03_variogram/00_fit_variogram.py new file mode 100644 index 0000000000000000000000000000000000000000..7334ed2c84a6e892a7028e7a73f650392f3880ff --- /dev/null +++ b/GSTools/source/examples/03_variogram/00_fit_variogram.py @@ -0,0 +1,36 @@ +""" +Fit Variogram +------------- +""" + +import numpy as np + +import gstools as gs + +############################################################################### +# Generate a synthetic field with an exponential model. + +x = np.random.RandomState(19970221).rand(1000) * 100.0 +y = np.random.RandomState(20011012).rand(1000) * 100.0 +model = gs.Exponential(dim=2, var=2, len_scale=8) +srf = gs.SRF(model, mean=0, seed=19970221) +field = srf((x, y)) + +############################################################################### +# Estimate the variogram of the field with 40 bins. + +bins = np.arange(40) +bin_center, gamma = gs.vario_estimate((x, y), field, bins) + +############################################################################### +# Fit the variogram with a stable model (no nugget fitted). + +fit_model = gs.Stable(dim=2) +fit_model.fit_variogram(bin_center, gamma, nugget=False) + +############################################################################### +# Plot the fitting result. + +ax = fit_model.plot(x_max=40) +ax.scatter(bin_center, gamma) +print(fit_model) diff --git a/GSTools/source/examples/03_variogram/01_find_best_model.py b/GSTools/source/examples/03_variogram/01_find_best_model.py new file mode 100644 index 0000000000000000000000000000000000000000..eab031cf6c5cd716428bcd0e6bdc9eab6b0f53c5 --- /dev/null +++ b/GSTools/source/examples/03_variogram/01_find_best_model.py @@ -0,0 +1,64 @@ +""" +Finding the best fitting variogram model +---------------------------------------- +""" + +import numpy as np +from matplotlib import pyplot as plt + +import gstools as gs + +############################################################################### +# Generate a synthetic field with an exponential model. + +x = np.random.RandomState(19970221).rand(1000) * 100.0 +y = np.random.RandomState(20011012).rand(1000) * 100.0 +model = gs.Exponential(dim=2, var=2, len_scale=8) +srf = gs.SRF(model, mean=0, seed=19970221) +field = srf((x, y)) + +############################################################################### +# Estimate the variogram of the field with 40 bins and plot the result. + +bins = np.arange(40) +bin_center, gamma = gs.vario_estimate((x, y), field, bins) + +############################################################################### +# Define a set of models to test. + +models = { + "Gaussian": gs.Gaussian, + "Exponential": gs.Exponential, + "Matern": gs.Matern, + "Stable": gs.Stable, + "Rational": gs.Rational, + "Circular": gs.Circular, + "Spherical": gs.Spherical, + "SuperSpherical": gs.SuperSpherical, + "JBessel": gs.JBessel, +} +scores = {} + +############################################################################### +# Iterate over all models, fit their variogram and calculate the r2 score. + +# plot the estimated variogram +plt.scatter(bin_center, gamma, color="k", label="data") +ax = plt.gca() + +# fit all models to the estimated variogram +for model in models: + fit_model = models[model](dim=2) + para, pcov, r2 = fit_model.fit_variogram(bin_center, gamma, return_r2=True) + fit_model.plot(x_max=40, ax=ax) + scores[model] = r2 + +############################################################################### +# Create a ranking based on the score and determine the best models + +ranking = sorted(scores.items(), key=lambda item: item[1], reverse=True) +print("RANKING by Pseudo-r2 score") +for i, (model, score) in enumerate(ranking, 1): + print(f"{i:>6}. {model:>15}: {score:.5}") + +plt.show() diff --git a/GSTools/source/examples/03_variogram/02_multi_vario.py b/GSTools/source/examples/03_variogram/02_multi_vario.py new file mode 100644 index 0000000000000000000000000000000000000000..71048849d8e2fe7346fb82acc0936f97a4e3abaa --- /dev/null +++ b/GSTools/source/examples/03_variogram/02_multi_vario.py @@ -0,0 +1,44 @@ +""" +Multi-field variogram estimation +-------------------------------- + +In this example, we demonstrate how to estimate a variogram from multiple +fields on the same point-set that should have the same statistical properties. +""" + +import matplotlib.pyplot as plt +import numpy as np + +import gstools as gs + +x = np.random.RandomState(19970221).rand(1000) * 100.0 +y = np.random.RandomState(20011012).rand(1000) * 100.0 +model = gs.Exponential(dim=2, var=2, len_scale=8) +srf = gs.SRF(model, mean=0) + +############################################################################### +# Generate two synthetic fields with an exponential model. + +field1 = srf((x, y), seed=19970221) +field2 = srf((x, y), seed=20011012) +fields = [field1, field2] + +############################################################################### +# Now we estimate the variograms for both fields individually and then again +# simultaneously with only one call. + +bins = np.arange(40) +bin_center, gamma1 = gs.vario_estimate((x, y), field1, bins) +bin_center, gamma2 = gs.vario_estimate((x, y), field2, bins) +bin_center, gamma = gs.vario_estimate((x, y), fields, bins) + +############################################################################### +# Now we demonstrate that the mean variogram from both fields coincides +# with the joined estimated one. + +plt.plot(bin_center, gamma1, label="field 1") +plt.plot(bin_center, gamma2, label="field 2") +plt.plot(bin_center, gamma, label="joined fields") +plt.plot(bin_center, 0.5 * (gamma1 + gamma2), ":", label="field 1+2 mean") +plt.legend() +plt.show() diff --git a/GSTools/source/examples/03_variogram/03_directional_2d.py b/GSTools/source/examples/03_variogram/03_directional_2d.py new file mode 100644 index 0000000000000000000000000000000000000000..460c751317474634f2b8e6aad21ce50c669e614d --- /dev/null +++ b/GSTools/source/examples/03_variogram/03_directional_2d.py @@ -0,0 +1,66 @@ +""" +Directional variogram estimation and fitting in 2D +-------------------------------------------------- + +In this example, we demonstrate how to estimate a directional variogram by +setting the direction angles in 2D. + +Afterwards we will fit a model to this estimated variogram and show the result. +""" + +import numpy as np +from matplotlib import pyplot as plt + +import gstools as gs + +############################################################################### +# Generating synthetic field with anisotropy and a rotation of 22.5 degree. + +angle = np.pi / 8 +model = gs.Exponential(dim=2, len_scale=[10, 5], angles=angle) +x = y = range(101) +srf = gs.SRF(model, seed=123456) +field = srf((x, y), mesh_type="structured") + +############################################################################### +# Now we are going to estimate a directional variogram with an angular +# tolerance of 11.25 degree and a bandwith of 8. + +bins = range(0, 40, 2) +bin_center, dir_vario, counts = gs.vario_estimate( + *((x, y), field, bins), + direction=gs.rotated_main_axes(dim=2, angles=angle), + angles_tol=np.pi / 16, + bandwidth=8, + mesh_type="structured", + return_counts=True, +) + +############################################################################### +# Afterwards we can use the estimated variogram to fit a model to it: + +print("Original:") +print(model) +model.fit_variogram(bin_center, dir_vario) +print("Fitted:") +print(model) + +############################################################################### +# Plotting. + +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=[10, 5]) + +ax1.scatter(bin_center, dir_vario[0], label="emp. vario: pi/8") +ax1.scatter(bin_center, dir_vario[1], label="emp. vario: pi*5/8") +ax1.legend(loc="lower right") + +model.plot("vario_axis", axis=0, ax=ax1, x_max=40, label="fit on axis 0") +model.plot("vario_axis", axis=1, ax=ax1, x_max=40, label="fit on axis 1") +ax1.set_title("Fitting an anisotropic model") + +srf.plot(ax=ax2) +plt.show() + +############################################################################### +# Without fitting a model, we see that the correlation length in the main +# direction is greater than the transversal one. diff --git a/GSTools/source/examples/03_variogram/04_directional_3d.py b/GSTools/source/examples/03_variogram/04_directional_3d.py new file mode 100644 index 0000000000000000000000000000000000000000..6a8b6ddfa6230119139f4b1b111e24930f1d9382 --- /dev/null +++ b/GSTools/source/examples/03_variogram/04_directional_3d.py @@ -0,0 +1,98 @@ +""" +Directional variogram estimation and fitting in 3D +-------------------------------------------------- + +In this example, we demonstrate how to estimate a directional variogram by +setting the estimation directions in 3D. + +Afterwards we will fit a model to this estimated variogram and show the result. +""" + +import matplotlib.pyplot as plt +import numpy as np +from mpl_toolkits.mplot3d import Axes3D + +import gstools as gs + +############################################################################### +# Generating synthetic field with anisotropy and rotation by Tait-Bryan angles. + +dim = 3 +# rotation around z, y, x +angles = [np.deg2rad(90), np.deg2rad(45), np.deg2rad(22.5)] +model = gs.Gaussian(dim=3, len_scale=[16, 8, 4], angles=angles) +x = y = z = range(50) +pos = (x, y, z) +srf = gs.SRF(model, seed=1001) +field = srf.structured(pos) + +############################################################################### +# Here we generate the axes of the rotated coordinate system +# to get an impression what the rotation angles do. + +# All 3 axes of the rotated coordinate-system +main_axes = gs.rotated_main_axes(dim, angles) +axis1, axis2, axis3 = main_axes + +############################################################################### +# Now we estimate the variogram along the main axes. When the main axes are +# unknown, one would need to sample multiple directions and look for the one +# with the longest correlation length (flattest gradient). +# Then check the transversal directions and so on. + +bin_center, dir_vario, counts = gs.vario_estimate( + pos, + field, + direction=main_axes, + bandwidth=10, + sampling_size=2000, + sampling_seed=1001, + mesh_type="structured", + return_counts=True, +) + +############################################################################### +# Afterwards we can use the estimated variogram to fit a model to it. +# Note, that the rotation angles need to be set beforehand. + +print("Original:") +print(model) +model.fit_variogram(bin_center, dir_vario) +print("Fitted:") +print(model) + +############################################################################### +# Plotting main axes and the fitted directional variogram. + +fig = plt.figure(figsize=[10, 5]) +ax1 = fig.add_subplot(121, projection=Axes3D.name) +ax2 = fig.add_subplot(122) + +ax1.plot([0, axis1[0]], [0, axis1[1]], [0, axis1[2]], label="0.") +ax1.plot([0, axis2[0]], [0, axis2[1]], [0, axis2[2]], label="1.") +ax1.plot([0, axis3[0]], [0, axis3[1]], [0, axis3[2]], label="2.") +ax1.set_xlim(-1, 1) +ax1.set_ylim(-1, 1) +ax1.set_zlim(-1, 1) +ax1.set_xlabel("X") +ax1.set_ylabel("Y") +ax1.set_zlabel("Z") +ax1.set_title("Tait-Bryan main axis") +ax1.legend(loc="lower left") + +x_max = max(bin_center) +ax2.scatter(bin_center, dir_vario[0], label="0. axis") +ax2.scatter(bin_center, dir_vario[1], label="1. axis") +ax2.scatter(bin_center, dir_vario[2], label="2. axis") +model.plot("vario_axis", axis=0, ax=ax2, x_max=x_max, label="fit on axis 0") +model.plot("vario_axis", axis=1, ax=ax2, x_max=x_max, label="fit on axis 1") +model.plot("vario_axis", axis=2, ax=ax2, x_max=x_max, label="fit on axis 2") +ax2.set_title("Fitting an anisotropic model") +ax2.legend() + +plt.show() + +############################################################################### +# Also, let's have a look at the field. + +srf.plot() diff --git a/GSTools/source/examples/03_variogram/05_auto_fit_variogram.py b/GSTools/source/examples/03_variogram/05_auto_fit_variogram.py new file mode 100644 index 0000000000000000000000000000000000000000..2fcc7fbd6685ccd581a8958a3e566bb5cf19f522 --- /dev/null +++ b/GSTools/source/examples/03_variogram/05_auto_fit_variogram.py @@ -0,0 +1,37 @@ +""" +Fit Variogram with automatic binning +------------------------------------ +""" + +import numpy as np + +import gstools as gs + +############################################################################### +# Generate a synthetic field with an exponential model. + +x = np.random.RandomState(19970221).rand(1000) * 100.0 +y = np.random.RandomState(20011012).rand(1000) * 100.0 +model = gs.Exponential(dim=2, var=2, len_scale=8) +srf = gs.SRF(model, mean=0, seed=19970221) +field = srf((x, y)) +print(field.var()) +############################################################################### +# Estimate the variogram of the field with automatic binning. + +bin_center, gamma = gs.vario_estimate((x, y), field) +print("estimated bin number:", len(bin_center)) +print("maximal bin distance:", max(bin_center)) + +############################################################################### +# Fit the variogram with a stable model (no nugget fitted). + +fit_model = gs.Stable(dim=2) +fit_model.fit_variogram(bin_center, gamma, nugget=False) +print(fit_model) + +############################################################################### +# Plot the fitting result. + +ax = fit_model.plot(x_max=max(bin_center)) +ax.scatter(bin_center, gamma) diff --git a/GSTools/source/examples/03_variogram/06_auto_bin_latlon.py b/GSTools/source/examples/03_variogram/06_auto_bin_latlon.py new file mode 100644 index 0000000000000000000000000000000000000000..cc248ea1601deae8da1cf4bf56a2ef048382c730 --- /dev/null +++ b/GSTools/source/examples/03_variogram/06_auto_bin_latlon.py @@ -0,0 +1,90 @@ +""" +Automatic binning with lat-lon data +----------------------------------- + +In this example we demonstrate automatic binning for a tiny data set +containing temperature records from Germany +(See the detailed DWD example for more information on the data). + +We use a data set from 20 meteo-stations choosen randomly. +""" + +import numpy as np + +import gstools as gs + +# lat, lon, temperature +data = np.array( + [ + [52.9336, 8.237, 15.7], + [48.6159, 13.0506, 13.9], + [52.4853, 7.9126, 15.1], + [50.7446, 9.345, 17.0], + [52.9437, 12.8518, 21.9], + [53.8633, 8.1275, 11.9], + [47.8342, 10.8667, 11.4], + [51.0881, 12.9326, 17.2], + [48.406, 11.3117, 12.9], + [49.7273, 8.1164, 17.2], + [49.4691, 11.8546, 13.4], + [48.0197, 12.2925, 13.9], + [50.4237, 7.4202, 18.1], + [53.0316, 13.9908, 21.3], + [53.8412, 13.6846, 21.3], + [54.6792, 13.4343, 17.4], + [49.9694, 9.9114, 18.6], + [51.3745, 11.292, 20.2], + [47.8774, 11.3643, 12.7], + [50.5908, 12.7139, 15.8], + ] +) +pos = data.T[:2] # lat, lon +field = data.T[2] # temperature + +############################################################################### +# Since the overall range of these meteo-stations is too low, we can use the +# data-variance as additional information during the fit of the variogram. + +emp_v = gs.vario_estimate(pos, field, latlon=True, geo_scale=gs.KM_SCALE) +sph = gs.Spherical(latlon=True, geo_scale=gs.KM_SCALE) +sph.fit_variogram(*emp_v, sill=np.var(field)) +ax = sph.plot("vario_yadrenko", x_max=2 * np.max(emp_v[0])) +ax.scatter(*emp_v, label="Empirical variogram") +ax.legend() +print(sph) + +############################################################################### +# As we can see, the variogram fitting was successful and providing the data +# variance helped finding the right length-scale. +# +# Now, we'll use this covariance model to interpolate the given data with +# ordinary kriging. + +# enclosing box for data points +grid_lat = np.linspace(np.min(pos[0]), np.max(pos[0])) +grid_lon = np.linspace(np.min(pos[1]), np.max(pos[1])) +# ordinary kriging +krige = gs.krige.Ordinary(sph, pos, field) +krige((grid_lat, grid_lon), mesh_type="structured") +ax = krige.plot() +# plotting lat on y-axis and lon on x-axis +ax.scatter(pos[1], pos[0], 50, c=field, edgecolors="k", label="input") +ax.legend() + +############################################################################### +# Looks good, doesn't it? +# +# This workflow is also implemented in the :any:`Krige` class, by setting +# ``fit_variogram=True``. Then the whole procedure shortens: + +krige = gs.krige.Ordinary(sph, pos, field, fit_variogram=True) +krige.structured((grid_lat, grid_lon)) + +# plot the result +krige.plot() +# show the fitting results +print(krige.model) + +############################################################################### +# This example shows, that setting up variogram estimation and kriging routines +# is straight forward with GSTools! diff --git a/GSTools/source/examples/03_variogram/README.rst b/GSTools/source/examples/03_variogram/README.rst new file mode 100644 index 0000000000000000000000000000000000000000..8eb42a8a8955e8f171aef210714a4f79d8c2ee74 --- /dev/null +++ b/GSTools/source/examples/03_variogram/README.rst @@ -0,0 +1,14 @@ +Variogram Estimation +==================== + +Estimating the spatial correlations is an important part of geostatistics. +These spatial correlations can be expressed by the variogram, which can be +estimated with the subpackage :any:`gstools.variogram`. The variograms can be +estimated on structured and unstructured grids. + +The same `(semi-)variogram `_ as +:ref:`tutorial_02_cov` is being used +by this subpackage. + +Examples +-------- diff --git a/GSTools/source/examples/04_vector_field/00_2d_vector_field.py b/GSTools/source/examples/04_vector_field/00_2d_vector_field.py new file mode 100644 index 0000000000000000000000000000000000000000..2e7227642c720613d74ea0512c81de974209b950 --- /dev/null +++ b/GSTools/source/examples/04_vector_field/00_2d_vector_field.py @@ -0,0 +1,47 @@ +""" +Generating a Random 2D Vector Field +----------------------------------- + +As a first example we are going to generate a 2d vector field with a Gaussian +covariance model on a structured grid: +""" + +import numpy as np + +import gstools as gs + +# the grid +x = np.arange(100) +y = np.arange(100) + +# a smooth Gaussian covariance model +model = gs.Gaussian(dim=2, var=1, len_scale=10) +srf = gs.SRF(model, generator="VectorField", seed=19841203) +srf((x, y), mesh_type="structured") +srf.plot() + +############################################################################### +# Let us have a look at the influence of the covariance model. Choosing the +# exponential model and keeping all other parameters the same + +# a rougher exponential covariance model +model2 = gs.Exponential(dim=2, var=1, len_scale=10) +srf.model = model2 +srf((x, y), mesh_type="structured", seed=19841203) +srf.plot() + +############################################################################### +# and we see, that the wiggles are much "rougher" than the smooth Gaussian ones. + + +############################################################################### +# Applications +# ~~~~~~~~~~~~ +# +# One great advantage of the Kraichnan method is, that after some initializations, +# one can compute the velocity field at arbitrary points, online, with hardly any +# overhead. +# This means, that for a Lagrangian transport simulation for example, the velocity +# can be evaluated at each particle position very efficiently and without any +# interpolation. These field interpolations are a common problem for Lagrangian +# methods. diff --git a/GSTools/source/examples/04_vector_field/01_3d_vector_field.py b/GSTools/source/examples/04_vector_field/01_3d_vector_field.py new file mode 100644 index 0000000000000000000000000000000000000000..6a1caa37c1e8e41a3e305bd292b2220b5c0b16a5 --- /dev/null +++ b/GSTools/source/examples/04_vector_field/01_3d_vector_field.py @@ -0,0 +1,64 @@ +""" +Generating a Random 3D Vector Field +----------------------------------- + +In this example we are going to generate a random 3D vector field with a +Gaussian covariance model. The mesh on which we generate the field will be +externally defined and it will be generated by PyVista. +""" + +# sphinx_gallery_thumbnail_path = 'pics/GS_3d_vector_field.png' +import pyvista as pv + +import gstools as gs + +# mainly for setting a white background +pv.set_plot_theme("document") + +############################################################################### +# create a uniform grid with PyVista +dims, spacing, origin = (40, 30, 10), (1, 1, 1), (-10, 0, 0) +mesh = pv.ImageData(dimensions=dims, spacing=spacing, origin=origin) + +############################################################################### +# create an incompressible random 3d velocity field on the given mesh +# with added mean velocity in x-direction +model = gs.Gaussian(dim=3, var=3, len_scale=1.5) +srf = gs.SRF(model, mean=(0.5, 0, 0), generator="VectorField", seed=198412031) +srf.mesh(mesh, points="points", name="Velocity") + +############################################################################### +# Now, we can do the plotting +streamlines = mesh.streamlines( + "Velocity", + terminal_speed=0.0, + n_points=800, + source_radius=2.5, +) + +# set a fancy camera position +cpos = [(25, 23, 17), (0, 10, 0), (0, 0, 1)] + +p = pv.Plotter() +# adding an outline might help navigating in 3D space +# p.add_mesh(mesh.outline(), color="k") +p.add_mesh( + streamlines.tube(radius=0.005), + show_scalar_bar=False, + diffuse=0.5, + ambient=0.5, +) + +############################################################################### +# .. note:: +# PyVista is not working on readthedocs, but you can try it out yourself by +# uncommenting the following line of code. + +# p.show(cpos=cpos) + +############################################################################### +# The result should look like this: +# +# .. image:: ../../pics/GS_3d_vector_field.png +# :width: 400px +# :align: center diff --git a/GSTools/source/examples/04_vector_field/README.rst b/GSTools/source/examples/04_vector_field/README.rst new file mode 100644 index 0000000000000000000000000000000000000000..cbe3971258ac39c49df644b2c6b623f4ac6526e9 --- /dev/null +++ b/GSTools/source/examples/04_vector_field/README.rst @@ -0,0 +1,37 @@ +Random Vector Field Generation +============================== + +In 1970, Kraichnan was the first to suggest a randomization method. +For studying the diffusion of single particles in a random incompressible +velocity field, he came up with a randomization method which includes a +projector which ensures the incompressibility of the vector field. + + +Without loss of generality we assume that the mean velocity :math:`\bar{U}` is oriented +towards the direction of the first basis vector :math:`\mathbf{e}_1`. Our goal is now to +generate random fluctuations with a given covariance model around this mean velocity. +And at the same time, making sure that the velocity field remains incompressible or +in other words, ensure :math:`\nabla \cdot \mathbf U = 0`. +This can be done by using the randomization method we already know, but adding a +projector to every mode being summed: + + +.. math:: + + \mathbf{U}(\mathbf{x}) = \bar{U} \mathbf{e}_1 - \sqrt{\frac{\sigma^{2}}{N}} + \sum_{i=1}^{N} \mathbf{p}(\mathbf{k}_i) \left[ Z_{1,i} + \cos\left( \langle \mathbf{k}_{i}, \mathbf{x} \rangle \right) + + \sin\left( \langle \mathbf{k}_{i}, \mathbf{x} \rangle \right) \right] + +with the projector + +.. math:: + + \mathbf{p}(\mathbf{k}_i) = \mathbf{e}_1 - \frac{\mathbf{k}_i k_1}{k^2} \; . + +By calculating :math:`\nabla \cdot \mathbf U = 0`, it can be verified, that +the resulting field is indeed incompressible. + + +Examples +-------- diff --git a/GSTools/source/examples/05_kriging/00_simple_kriging.py b/GSTools/source/examples/05_kriging/00_simple_kriging.py new file mode 100644 index 0000000000000000000000000000000000000000..1a245b6c388a78b3d1b78cb3208bbd80bd2289b4 --- /dev/null +++ b/GSTools/source/examples/05_kriging/00_simple_kriging.py @@ -0,0 +1,50 @@ +r""" +Simple Kriging +-------------- + +Simple kriging assumes a known mean of the data. +For simplicity we assume a mean of 0, +which can be achieved by subtracting the mean from the observed values and +subsequently adding it to the resulting data. + +The resulting equation system for :math:`W` is given by: + +.. math:: + + W = \begin{pmatrix}c(x_1,x_1) & \cdots & c(x_1,x_n) \\ + \vdots & \ddots & \vdots \\ + c(x_n,x_1) & \cdots & c(x_n,x_n) + \end{pmatrix}^{-1} + \begin{pmatrix}c(x_1,x_0) \\ \vdots \\ c(x_n,x_0) \end{pmatrix} + +Thereby :math:`c(x_i,x_j)` is the covariance of the given observations. + + +Example +^^^^^^^ + +Here we use simple kriging in 1D (for plotting reasons) with 5 given observations/conditions. +The mean of the field has to be given beforehand. + +""" + +import numpy as np + +from gstools import Gaussian, krige + +# condtions +cond_pos = [0.3, 1.9, 1.1, 3.3, 4.7] +cond_val = [0.47, 0.56, 0.74, 1.47, 1.74] +# resulting grid +gridx = np.linspace(0.0, 15.0, 151) +# spatial random field class +model = Gaussian(dim=1, var=0.5, len_scale=2) + +############################################################################### +krig = krige.Simple(model, mean=1, cond_pos=cond_pos, cond_val=cond_val) +krig(gridx) + +############################################################################### +ax = krig.plot() +ax.scatter(cond_pos, cond_val, color="k", zorder=10, label="Conditions") +ax.legend() diff --git a/GSTools/source/examples/05_kriging/01_ordinary_kriging.py b/GSTools/source/examples/05_kriging/01_ordinary_kriging.py new file mode 100644 index 0000000000000000000000000000000000000000..d26254ef29dea1667c585ea7c7b29227c9d3614a --- /dev/null +++ b/GSTools/source/examples/05_kriging/01_ordinary_kriging.py @@ -0,0 +1,50 @@ +r""" +Ordinary Kriging +---------------- + +Ordinary kriging will estimate an appropriate mean of the field, +based on the given observations/conditions and the covariance model used. + +The resulting system of equations for :math:`W` is given by: + +.. math:: + + \begin{pmatrix}W\\\mu\end{pmatrix} = \begin{pmatrix} + c(x_1,x_1) & \cdots & c(x_1,x_n) &1 \\ + \vdots & \ddots & \vdots & \vdots \\ + c(x_n,x_1) & \cdots & c(x_n,x_n) & 1 \\ + 1 &\cdots& 1 & 0 + \end{pmatrix}^{-1} + \begin{pmatrix}c(x_1,x_0) \\ \vdots \\ c(x_n,x_0) \\ 1\end{pmatrix} + +Thereby :math:`c(x_i,x_j)` is the covariance of the given observations +and :math:`\mu` is a Lagrange multiplier to minimize the kriging error and estimate the mean. + + +Example +^^^^^^^ + +Here we use ordinary kriging in 1D (for plotting reasons) with 5 given observations/conditions. +The estimated mean can be accessed by ``krig.mean``. +""" + +import numpy as np + +from gstools import Gaussian, krige + +# condtions +cond_pos = [0.3, 1.9, 1.1, 3.3, 4.7] +cond_val = [0.47, 0.56, 0.74, 1.47, 1.74] +# resulting grid +gridx = np.linspace(0.0, 15.0, 151) +# spatial random field class +model = Gaussian(dim=1, var=0.5, len_scale=2) + +############################################################################### +krig = krige.Ordinary(model, cond_pos=cond_pos, cond_val=cond_val) +krig(gridx) + +############################################################################### +ax = krig.plot() +ax.scatter(cond_pos, cond_val, color="k", zorder=10, label="Conditions") +ax.legend() diff --git a/GSTools/source/examples/05_kriging/02_pykrige_interface.py b/GSTools/source/examples/05_kriging/02_pykrige_interface.py new file mode 100644 index 0000000000000000000000000000000000000000..a6fbf03e159a71d69145bdc60b3278197eb80733 --- /dev/null +++ b/GSTools/source/examples/05_kriging/02_pykrige_interface.py @@ -0,0 +1,64 @@ +""" +Interface to PyKrige +-------------------- + +To use fancier methods like +`regression kriging `__, +we provide an interface to +`PyKrige `__ (>v1.5), which means +you can pass a GSTools covariance model to the kriging routines of PyKrige. + +To demonstrate the general workflow, we compare ordinary kriging of PyKrige +with the corresponding GSTools routine in 2D: +""" + +import numpy as np +from matplotlib import pyplot as plt +from pykrige.ok import OrdinaryKriging + +import gstools as gs + +# conditioning data +cond_x = [0.3, 1.9, 1.1, 3.3, 4.7] +cond_y = [1.2, 0.6, 3.2, 4.4, 3.8] +cond_val = [0.47, 0.56, 0.74, 1.47, 1.74] + +# grid definition for output field +gridx = np.arange(0.0, 5.5, 0.1) +gridy = np.arange(0.0, 6.5, 0.1) + +############################################################################### +# A GSTools based :any:`Gaussian` covariance model: + +model = gs.Gaussian( + dim=2, len_scale=1, anis=0.2, angles=-0.5, var=0.5, nugget=0.1 +) + +############################################################################### +# Ordinary Kriging with PyKrige +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +# +# One can pass the defined GSTools model as +# variogram model, which will `not` be fitted to the given data. +# By providing the GSTools model, rotation and anisotropy are also +# automatically defined: + +OK1 = OrdinaryKriging(cond_x, cond_y, cond_val, variogram_model=model) +z1, ss1 = OK1.execute("grid", gridx, gridy) +plt.imshow(z1, origin="lower") +plt.show() + +############################################################################### +# Ordinary Kriging with GSTools +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +# +# The :any:`Ordinary` kriging class is provided by GSTools as a shortcut to +# define ordinary kriging with the general :any:`Krige` class. +# +# PyKrige's routines are using exact kriging by default (when given a nugget). +# To reproduce this behavior in GSTools, we have to set ``exact=True``. + +OK2 = gs.krige.Ordinary(model, [cond_x, cond_y], cond_val, exact=True) +OK2.structured([gridx, gridy]) +ax = OK2.plot() +ax.set_aspect("equal") diff --git a/GSTools/source/examples/05_kriging/03_compare_kriging.py b/GSTools/source/examples/05_kriging/03_compare_kriging.py new file mode 100644 index 0000000000000000000000000000000000000000..463faa0a663c80a0af256f32a651819781f529cf --- /dev/null +++ b/GSTools/source/examples/05_kriging/03_compare_kriging.py @@ -0,0 +1,36 @@ +""" +Compare Kriging +--------------- +""" + +import matplotlib.pyplot as plt +import numpy as np + +from gstools import Gaussian, krige + +# condtions +cond_pos = [0.3, 1.9, 1.1, 3.3, 4.7] +cond_val = [0.47, 0.56, 0.74, 1.47, 1.74] +# resulting grid +gridx = np.linspace(0.0, 15.0, 151) + +############################################################################### +# A gaussian variogram model. + +model = Gaussian(dim=1, var=0.5, len_scale=2) + +############################################################################### +# Two kriged fields. One with simple and one with ordinary kriging. + +kr1 = krige.Simple(model=model, mean=1, cond_pos=cond_pos, cond_val=cond_val) +kr2 = krige.Ordinary(model=model, cond_pos=cond_pos, cond_val=cond_val) +kr1(gridx) +kr2(gridx) + +############################################################################### + +plt.plot(gridx, kr1.field, label="simple kriged field") +plt.plot(gridx, kr2.field, label="ordinary kriged field") +plt.scatter(cond_pos, cond_val, color="k", zorder=10, label="Conditions") +plt.legend() +plt.show() diff --git a/GSTools/source/examples/05_kriging/04_extdrift_kriging.py b/GSTools/source/examples/05_kriging/04_extdrift_kriging.py new file mode 100644 index 0000000000000000000000000000000000000000..2e6a168a53f539091b81aa27daac40c8c7bc6a41 --- /dev/null +++ b/GSTools/source/examples/05_kriging/04_extdrift_kriging.py @@ -0,0 +1,26 @@ +""" +External Drift Kriging +---------------------- +""" + +import numpy as np + +from gstools import SRF, Gaussian, krige + +# synthetic condtions with a drift +drift_model = Gaussian(dim=1, len_scale=4) +drift = SRF(drift_model, seed=1010) +cond_pos = [0.3, 1.9, 1.1, 3.3, 4.7] +ext_drift = drift(cond_pos) +cond_val = ext_drift * 2 + 1 +# resulting grid +gridx = np.linspace(0.0, 15.0, 151) +grid_drift = drift(gridx) +# kriging +model = Gaussian(dim=1, var=2, len_scale=4) +krig = krige.ExtDrift(model, cond_pos, cond_val, ext_drift) +krig(gridx, ext_drift=grid_drift) +ax = krig.plot() +ax.scatter(cond_pos, cond_val, color="k", zorder=10, label="Conditions") +ax.plot(gridx, grid_drift, label="drift") +ax.legend() diff --git a/GSTools/source/examples/05_kriging/05_universal_kriging.py b/GSTools/source/examples/05_kriging/05_universal_kriging.py new file mode 100644 index 0000000000000000000000000000000000000000..5501694ad03bf64190675e29ec32319d2ac66214 --- /dev/null +++ b/GSTools/source/examples/05_kriging/05_universal_kriging.py @@ -0,0 +1,41 @@ +""" +Universal Kriging +----------------- + +You can give a polynomial order or a list of self defined +functions representing the internal drift of the given values. +This drift will be fitted internally during the kriging interpolation. + +In the following we are creating artificial data, where a linear drift +was added. The resulting samples are then used as input for Universal kriging. + +The "linear" drift is then estimated during the interpolation. +To access only the estimated mean/drift, we provide a switch `only_mean` +in the call routine. +""" + +import numpy as np + +from gstools import SRF, Gaussian, krige + +# synthetic condtions with a drift +drift_model = Gaussian(dim=1, var=0.1, len_scale=2) +drift = SRF(drift_model, seed=101) +cond_pos = np.linspace(0.1, 8, 10) +cond_val = drift(cond_pos) + cond_pos * 0.1 + 1 +# resulting grid +gridx = np.linspace(0.0, 15.0, 151) +drift_field = drift(gridx) + gridx * 0.1 + 1 +# kriging +model = Gaussian(dim=1, var=0.1, len_scale=2) +krig = krige.Universal(model, cond_pos, cond_val, "linear") +krig(gridx) +ax = krig.plot() +ax.scatter(cond_pos, cond_val, color="k", zorder=10, label="Conditions") +ax.plot(gridx, gridx * 0.1 + 1, ":", label="linear drift") +ax.plot(gridx, drift_field, "--", label="original field") + +mean = krig(gridx, only_mean=True) +ax.plot(gridx, mean, label="estimated drift") + +ax.legend() diff --git a/GSTools/source/examples/05_kriging/06_detrended_kriging.py b/GSTools/source/examples/05_kriging/06_detrended_kriging.py new file mode 100644 index 0000000000000000000000000000000000000000..6d20cf1db923407d815d09960ecbaa36b16e1530 --- /dev/null +++ b/GSTools/source/examples/05_kriging/06_detrended_kriging.py @@ -0,0 +1,32 @@ +""" +Detrended Kriging +----------------- +""" + +import numpy as np + +from gstools import SRF, Gaussian, krige + + +def trend(x): + """Example for a simple linear trend.""" + return x * 0.1 + 1 + + +# synthetic condtions with trend/drift +drift_model = Gaussian(dim=1, var=0.1, len_scale=2) +drift = SRF(drift_model, seed=101) +cond_pos = np.linspace(0.1, 8, 10) +cond_val = drift(cond_pos) + trend(cond_pos) +# resulting grid +gridx = np.linspace(0.0, 15.0, 151) +drift_field = drift(gridx) + trend(gridx) +# kriging +model = Gaussian(dim=1, var=0.1, len_scale=2) +krig_trend = krige.Detrended(model, cond_pos, cond_val, trend) +krig_trend(gridx) +ax = krig_trend.plot() +ax.scatter(cond_pos, cond_val, color="k", zorder=10, label="Conditions") +ax.plot(gridx, trend(gridx), ":", label="linear trend") +ax.plot(gridx, drift_field, "--", label="original field") +ax.legend() diff --git a/GSTools/source/examples/05_kriging/07_detrended_ordinary_kriging.py b/GSTools/source/examples/05_kriging/07_detrended_ordinary_kriging.py new file mode 100644 index 0000000000000000000000000000000000000000..81d01744784264f98173b483be0eedf00144dd10 --- /dev/null +++ b/GSTools/source/examples/05_kriging/07_detrended_ordinary_kriging.py @@ -0,0 +1,32 @@ +""" +Detrended Ordinary Kriging +-------------------------- +""" + +import numpy as np + +from gstools import SRF, Gaussian, krige + + +def trend(x): + """Example for a simple linear trend.""" + return x * 0.1 + 1 + + +# synthetic condtions with trend/drift +drift_model = Gaussian(dim=1, var=0.1, len_scale=2) +drift = SRF(drift_model, seed=101) +cond_pos = np.linspace(0.1, 8, 10) +cond_val = drift(cond_pos) + trend(cond_pos) +# resulting grid +gridx = np.linspace(0.0, 15.0, 151) +drift_field = drift(gridx) + trend(gridx) +# kriging +model = Gaussian(dim=1, var=0.1, len_scale=2) +krig_trend = krige.Ordinary(model, cond_pos, cond_val, trend=trend) +krig_trend(gridx) +ax = krig_trend.plot() +ax.scatter(cond_pos, cond_val, color="k", zorder=10, label="Conditions") +ax.plot(gridx, trend(gridx), ":", label="linear trend") +ax.plot(gridx, drift_field, "--", label="original field") +ax.legend() diff --git a/GSTools/source/examples/05_kriging/08_measurement_errors.py b/GSTools/source/examples/05_kriging/08_measurement_errors.py new file mode 100644 index 0000000000000000000000000000000000000000..c2965ca0cd3696411db1ca7ae392e32f8d061419 --- /dev/null +++ b/GSTools/source/examples/05_kriging/08_measurement_errors.py @@ -0,0 +1,56 @@ +r""" +Incorporating measurement errors +-------------------------------- + +To incorporate the nugget effect and/or given measurement errors, +one can set `exact` to `False` and provide either individual measurement errors +for each point or set the nugget as a constant measurement error everywhere. + +In the following we will show the influence of the nugget and +measurement errors. +""" + +import numpy as np + +import gstools as gs + +# condtions +cond_pos = [0.3, 1.1, 1.9, 3.3, 4.7] +cond_val = [0.47, 0.74, 0.56, 1.47, 1.74] +cond_err = [0.01, 0.0, 0.1, 0.05, 0] +# resulting grid +gridx = np.linspace(0.0, 15.0, 151) +# spatial random field class +model = gs.Gaussian(dim=1, var=0.9, len_scale=1, nugget=0.1) + +############################################################################### +# Here we will use Simple kriging (`unbiased=False`) to interpolate the given +# conditions. + +krig = gs.Krige( + model=model, + cond_pos=cond_pos, + cond_val=cond_val, + mean=1, + unbiased=False, + exact=False, + cond_err=cond_err, +) +krig(gridx) + +############################################################################### +# Let's plot the data. You can see, that the estimated values differ more from +# the input, when the given measurement errors get bigger. +# In addition we plot the standard deviation. + +ax = krig.plot() +ax.scatter(cond_pos, cond_val, color="k", zorder=10, label="Conditions") +ax.fill_between( + gridx, + # plus/minus standard deviation (70 percent confidence interval) + krig.field - np.sqrt(krig.krige_var), + krig.field + np.sqrt(krig.krige_var), + alpha=0.3, + label="Standard deviation", +) +ax.legend() diff --git a/GSTools/source/examples/05_kriging/09_pseudo_inverse.py b/GSTools/source/examples/05_kriging/09_pseudo_inverse.py new file mode 100644 index 0000000000000000000000000000000000000000..7615d888657f0d4feadeacd7144a63379033b084 --- /dev/null +++ b/GSTools/source/examples/05_kriging/09_pseudo_inverse.py @@ -0,0 +1,40 @@ +r""" +Redundant data and pseudo-inverse +--------------------------------- + +It can happen, that the kriging system gets numerically unstable. +One reason could be, that the input data contains redundant conditioning points +that hold different values. + +To smoothly deal with such situations, you can use the pseudo +inverse for the kriging matrix, which is enabled by default. + +This will result in the average value for the redundant data. + +Example +^^^^^^^ + +In the following we have two different values at the same location. +The resulting kriging field will hold the average at this point. +""" + +import numpy as np + +from gstools import Gaussian, krige + +# condtions +cond_pos = [0.3, 1.9, 1.1, 3.3, 1.1] +cond_val = [0.47, 0.56, 0.74, 1.47, 1.14] +# resulting grid +gridx = np.linspace(0.0, 8.0, 81) +# spatial random field class +model = Gaussian(dim=1, var=0.5, len_scale=1) + +############################################################################### +krig = krige.Ordinary(model, cond_pos=cond_pos, cond_val=cond_val) +krig(gridx) + +############################################################################### +ax = krig.plot() +ax.scatter(cond_pos, cond_val, color="k", zorder=10, label="Conditions") +ax.legend() diff --git a/GSTools/source/examples/05_kriging/README.rst b/GSTools/source/examples/05_kriging/README.rst new file mode 100644 index 0000000000000000000000000000000000000000..ef92e425f4afbad0af803b287c4a24e68b02b06b --- /dev/null +++ b/GSTools/source/examples/05_kriging/README.rst @@ -0,0 +1,100 @@ +.. _tutorial_05_kriging: + +Kriging +======= + +The subpackage :py:mod:`gstools.krige` provides routines for Gaussian process regression, +also known as kriging. +Kriging is a method of data interpolation based on predefined covariance models. + +The aim of kriging is to derive the value of a field at some point :math:`x_0`, +when there are fixed observed values :math:`z(x_1)\ldots z(x_n)` at given points :math:`x_i`. + +The resluting value :math:`z_0` at :math:`x_0` is calculated as a weighted mean: + +.. math:: + + z_0 = \sum_{i=1}^n w_i \cdot z_i + +The weights :math:`W = (w_1,\ldots,w_n)` depent on the given covariance model and the location of the target point. + +The different kriging approaches provide different ways of calculating :math:`W`. + +The :any:`Krige` class provides everything in one place and you can switch on/off +the features you want: + +* `unbiased`: the weights have to sum up to `1`. If true, this results in + :any:`Ordinary` kriging, where the mean is estimated, otherwise it will result in + :any:`Simple` kriging, where the mean has to be given. +* `drift_functions`: you can give a polynomial order or a list of self defined + functions representing the internal drift of the given values. This drift will + be fitted internally during the kriging interpolation. This results in :any:`Universal` kriging. +* `ext_drift`: You can also give an external drift per point to the routine. + In contrast to the internal drift, that is evaluated at the desired points with + the given functions, the external drift has to given for each point form an "external" + source. This results in :any:`ExtDrift` kriging. +* `trend`, `mean`, `normalizer`: These are used to pre- and post-process data. + If you already have fitted a trend model that is provided as a callable function, + you can give it to the kriging routine. Normalizer are power-transformations + to gain normality. + `mean` behaves similar to `trend` but is applied at another position: + + 1. conditioning data is de-trended (substracting trend) + 2. detrended conditioning data is then normalized (in order to follow a normal distribution) + 3. normalized conditioning data is set to zero mean (subtracting mean) + + Cosequently, when there is no normalizer given, trend and mean are the same thing + and only one should be used. + :any:`Detrended` kriging is a shortcut to provide only a trend and simple kriging + with normal data. +* `exact` and `cond_err`: To incorporate the nugget effect and/or measurement errors, + one can set `exact` to `False` and provide either individual measurement errors + for each point or set the nugget as a constant measurement error everywhere. +* `pseudo_inv`: Sometimes the inversion of the kriging matrix can be numerically unstable. + This occurs for examples in cases of redundant input values. In this case we provide a switch to + use the pseudo-inverse of the matrix. Then redundant conditional values will automatically + be averaged. + +.. note:: + + All mentioned features can be combined within the :any:`Krige` class. + All other kriging classes are just shortcuts to this class with a limited list + of input parameters. + +The routines for kriging are almost identical to the routines for spatial random fields, +with regard to their handling. +First you define a covariance model, as described in :ref:`tutorial_02_cov`, +then you initialize the kriging class with this model: + +.. code-block:: python + + import gstools as gs + # condtions + cond_pos = [...] + cond_val = [...] + model = gs.Gaussian(dim=1, var=0.5, len_scale=2) + krig = gs.krige.Simple(model, cond_pos=cond_pos, cond_val=cond_val, mean=1) + +The resulting field instance ``krig`` has the same methods as the +:any:`SRF` class. +You can call it to evaluate the kriged field at different points, +you can plot the latest field or you can export the field and so on. + +Provided Kriging Methods +------------------------ + +.. currentmodule:: gstools.krige + +The following kriging methods are provided within the +submodule :any:`gstools.krige`. + +.. autosummary:: + Krige + Simple + Ordinary + Universal + ExtDrift + Detrended + +Examples +-------- diff --git a/GSTools/source/examples/06_conditioned_fields/00_condition_ensemble.py b/GSTools/source/examples/06_conditioned_fields/00_condition_ensemble.py new file mode 100644 index 0000000000000000000000000000000000000000..5cc07eedff11fda3e93b4ab56273fe9f2e27514a --- /dev/null +++ b/GSTools/source/examples/06_conditioned_fields/00_condition_ensemble.py @@ -0,0 +1,63 @@ +""" +Conditioning with Ordinary Kriging +---------------------------------- + +Here we use ordinary kriging in 1D (for plotting reasons) +with 5 given observations/conditions, +to generate an ensemble of conditioned random fields. +""" + +import matplotlib.pyplot as plt +import numpy as np + +import gstools as gs + +# condtions +cond_pos = [0.3, 1.9, 1.1, 3.3, 4.7] +cond_val = [0.47, 0.56, 0.74, 1.47, 1.74] +gridx = np.linspace(0.0, 15.0, 151) + +############################################################################### +# The conditioned spatial random field class depends on a Krige class in order +# to handle the conditions. +# This is created as described in the kriging tutorial. +# +# Here we use a Gaussian covariance model and ordinary kriging for conditioning +# the spatial random field. + +model = gs.Gaussian(dim=1, var=0.5, len_scale=1.5) +krige = gs.krige.Ordinary(model, cond_pos, cond_val) +cond_srf = gs.CondSRF(krige) +cond_srf.set_pos(gridx) + +############################################################################### +# To generate the ensemble we will use a seed-generator. +# We can specify individual names for each field by the keyword `store`: + +seed = gs.random.MasterRNG(20170519) +for i in range(100): + cond_srf(seed=seed(), store=f"f{i}") + label = "Conditioned ensemble" if i == 0 else None + plt.plot(gridx, cond_srf[f"f{i}"], color="k", alpha=0.1, label=label) + +fields = [cond_srf[f"f{i}"] for i in range(100)] +plt.plot(gridx, cond_srf.krige(only_mean=True), label="estimated mean") +plt.plot(gridx, np.mean(fields, axis=0), linestyle=":", label="Ensemble mean") +plt.plot(gridx, cond_srf.krige.field, linestyle="dashed", label="kriged field") +plt.scatter(cond_pos, cond_val, color="k", zorder=10, label="Conditions") +# 99 percent confidence interval +conf = gs.tools.confidence_scaling(0.99) +plt.fill_between( + gridx, + cond_srf.krige.field - conf * np.sqrt(cond_srf.krige.krige_var), + cond_srf.krige.field + conf * np.sqrt(cond_srf.krige.krige_var), + alpha=0.3, + label="99% confidence interval", +) +plt.legend() +plt.show() + +############################################################################### +# As you can see, the kriging field coincides with the ensemble mean of the +# conditioned random fields and the estimated mean +# is the mean of the far-field. diff --git a/GSTools/source/examples/06_conditioned_fields/01_2D_condition_ensemble.py b/GSTools/source/examples/06_conditioned_fields/01_2D_condition_ensemble.py new file mode 100644 index 0000000000000000000000000000000000000000..81f514647ae1d527d3ecda7808f779d789684499 --- /dev/null +++ b/GSTools/source/examples/06_conditioned_fields/01_2D_condition_ensemble.py @@ -0,0 +1,71 @@ +""" +Creating an Ensemble of conditioned 2D Fields +--------------------------------------------- + +Let's create an ensemble of conditioned random fields in 2D. +""" + +import matplotlib.pyplot as plt +import numpy as np + +import gstools as gs + +# conditioning data (x, y, value) +cond_pos = [[0.3, 1.9, 1.1, 3.3, 4.7], [1.2, 0.6, 3.2, 4.4, 3.8]] +cond_val = [0.47, 0.56, 0.74, 1.47, 1.74] + +# grid definition for output field +x = np.arange(0, 5, 0.1) +y = np.arange(0, 5, 0.1) + +model = gs.Gaussian(dim=2, var=0.5, len_scale=5, anis=0.5, angles=-0.5) +krige = gs.Krige(model, cond_pos=cond_pos, cond_val=cond_val) +cond_srf = gs.CondSRF(krige) +cond_srf.set_pos([x, y], "structured") + +############################################################################### +# To generate the ensemble we will use a seed-generator. +# By specifying ``store=[f"fld{i}", False, False]``, only the conditioned field +# is stored with the specified name. The raw random field and the raw kriging +# field is not stored. This way, we can access each conditioned field by index +# ``cond_srf[i]``: + +seed = gs.random.MasterRNG(20170519) +ens_no = 4 +for i in range(ens_no): + cond_srf(seed=seed(), store=[f"fld{i}", False, False]) + +############################################################################### +# Now let's have a look at the pairwise differences between the generated +# fields. We will see, that they coincide at the given conditions. + +fig, ax = plt.subplots(ens_no + 1, ens_no + 1, figsize=(8, 8)) +# plotting kwargs for scatter and image +vmax = np.max(cond_srf.all_fields) +sc_kw = dict(c=cond_val, edgecolors="k", vmin=0, vmax=vmax) +im_kw = dict(extent=2 * [0, 5], origin="lower", vmin=0, vmax=vmax) +for i in range(ens_no): + # conditioned fields and conditions + ax[i + 1, 0].imshow(cond_srf[i].T, **im_kw) + ax[i + 1, 0].scatter(*cond_pos, **sc_kw) + ax[i + 1, 0].set_ylabel(f"Field {i}", fontsize=10) + ax[0, i + 1].imshow(cond_srf[i].T, **im_kw) + ax[0, i + 1].scatter(*cond_pos, **sc_kw) + ax[0, i + 1].set_title(f"Field {i}", fontsize=10) + # absolute differences + for j in range(ens_no): + ax[i + 1, j + 1].imshow(np.abs(cond_srf[i] - cond_srf[j]).T, **im_kw) + +# beautify plots +ax[0, 0].axis("off") +for a in ax.flatten(): + a.set_xticklabels([]), a.set_yticklabels([]) + a.set_xticks([]), a.set_yticks([]) +fig.subplots_adjust(wspace=0, hspace=0) +fig.show() + +############################################################################### +# To check if the generated fields are correct, we can have a look at their +# names: + +print(cond_srf.field_names) diff --git a/GSTools/source/examples/06_conditioned_fields/README.rst b/GSTools/source/examples/06_conditioned_fields/README.rst new file mode 100644 index 0000000000000000000000000000000000000000..4d7e6798106942def4229eb90ee631937554696a --- /dev/null +++ b/GSTools/source/examples/06_conditioned_fields/README.rst @@ -0,0 +1,26 @@ +Conditioned Fields +================== + +Kriged fields tend to approach the field mean outside the area of observations. +To generate random fields, that coincide with given observations, but are still +random according to a given covariance model away from the observations proximity, +we provide the generation of conditioned random fields. + +The idea behind conditioned random fields builds up on kriging. +First we generate a field with a kriging method, then we generate a random field, +with 0 as mean and 1 as variance that will be multiplied with the kriging +standard deviation. + +To do so, you can instantiate a :any:`CondSRF` class with a configured +:any:`Krige` class. + +The setup of the a conditioned random field should be as follows: + +.. code-block:: python + + krige = gs.Krige(model, cond_pos, cond_val) + cond_srf = gs.CondSRF(krige) + field = cond_srf(grid) + +Examples +-------- diff --git a/GSTools/source/examples/07_transformations/00_log_normal.py b/GSTools/source/examples/07_transformations/00_log_normal.py new file mode 100644 index 0000000000000000000000000000000000000000..d44c16270e9f69fafe807d53ae697b855e8ca927 --- /dev/null +++ b/GSTools/source/examples/07_transformations/00_log_normal.py @@ -0,0 +1,18 @@ +""" +log-normal fields +----------------- + +Here we transform a field to a log-normal distribution: + +See :any:`transform.normal_to_lognormal` +""" + +import gstools as gs + +# structured field with a size of 100x100 and a grid-size of 1x1 +x = y = range(100) +model = gs.Gaussian(dim=2, var=1, len_scale=10) +srf = gs.SRF(model, seed=20170519) +srf.structured([x, y]) +srf.transform("normal_to_lognormal") # also "lognormal" works +srf.plot() diff --git a/GSTools/source/examples/07_transformations/01_binary.py b/GSTools/source/examples/07_transformations/01_binary.py new file mode 100644 index 0000000000000000000000000000000000000000..125e29d06dd726e9877f36baafc7da12ea3e8186 --- /dev/null +++ b/GSTools/source/examples/07_transformations/01_binary.py @@ -0,0 +1,20 @@ +""" +binary fields +------------- + +Here we transform a field to a binary field with only two values. +The dividing value is the mean by default and the upper and lower values +are derived to preserve the variance. + +See :any:`transform.binary` +""" + +import gstools as gs + +# structured field with a size of 100x100 and a grid-size of 1x1 +x = y = range(100) +model = gs.Gaussian(dim=2, var=1, len_scale=10) +srf = gs.SRF(model, seed=20170519) +srf.structured([x, y]) +srf.transform("binary") +srf.plot() diff --git a/GSTools/source/examples/07_transformations/02_discrete.py b/GSTools/source/examples/07_transformations/02_discrete.py new file mode 100644 index 0000000000000000000000000000000000000000..48f67a2d7d04c39ce22ce6db544eafba05b7a846 --- /dev/null +++ b/GSTools/source/examples/07_transformations/02_discrete.py @@ -0,0 +1,45 @@ +""" +Discrete fields +--------------- + +Here we transform a field to a discrete field with values. +If we do not give thresholds, the pairwise means of the given +values are taken as thresholds. +If thresholds are given, arbitrary values can be applied to the field. + +See :any:`transform.discrete` +""" + +import numpy as np + +import gstools as gs + +# Structured field with a size of 100x100 and a grid-size of 0.5x0.5 +x = y = np.arange(200) * 0.5 +model = gs.Gaussian(dim=2, var=1, len_scale=5) +srf = gs.SRF(model, seed=20170519) +srf.structured([x, y]) + +############################################################################### +# Create 5 equidistanly spaced values, thresholds are the arithmetic means + +values1 = np.linspace(np.min(srf.field), np.max(srf.field), 5) +srf.transform("discrete", store="f1", values=values1) +srf.plot("f1") + +############################################################################### +# Calculate thresholds for equal shares +# but apply different values to the separated classes + +values2 = [0, -1, 2, -3, 4] +srf.transform("discrete", store="f2", values=values2, thresholds="equal") +srf.plot("f2") + +############################################################################### +# Create user defined thresholds +# and apply different values to the separated classes + +values3 = [0, 1, 10] +thresholds = [-1, 1] +srf.transform("discrete", store="f3", values=values3, thresholds=thresholds) +srf.plot("f3") diff --git a/GSTools/source/examples/07_transformations/03_zinn_harvey.py b/GSTools/source/examples/07_transformations/03_zinn_harvey.py new file mode 100644 index 0000000000000000000000000000000000000000..fad1fb64ede9e4175cc5203b3b3499305c50312f --- /dev/null +++ b/GSTools/source/examples/07_transformations/03_zinn_harvey.py @@ -0,0 +1,21 @@ +""" +Zinn & Harvey transformation +---------------------------- + +Here, we transform a field with the so called "Zinn & Harvey" transformation presented in +`Zinn & Harvey (2003) `__. +With this transformation, one could overcome the restriction that in ordinary +Gaussian random fields the mean values are the ones being the most connected. + +See :any:`transform.zinnharvey` +""" + +import gstools as gs + +# structured field with a size of 100x100 and a grid-size of 1x1 +x = y = range(100) +model = gs.Gaussian(dim=2, var=1, len_scale=10) +srf = gs.SRF(model, seed=20170519) +srf.structured([x, y]) +srf.transform("zinnharvey", conn="high") +srf.plot() diff --git a/GSTools/source/examples/07_transformations/04_bimodal.py b/GSTools/source/examples/07_transformations/04_bimodal.py new file mode 100644 index 0000000000000000000000000000000000000000..4dd6fb2987f9885314b55e62d43043366162e15b --- /dev/null +++ b/GSTools/source/examples/07_transformations/04_bimodal.py @@ -0,0 +1,23 @@ +""" +Bimodal fields +-------------- + +We provide two transformations to obtain bimodal distributions: + +* `arcsin `__. +* `uquad `__. + +Both transformations will preserve the mean and variance of the given field by default. + +See: :any:`transform.normal_to_arcsin` and :any:`transform.normal_to_uquad` +""" + +import gstools as gs + +# structured field with a size of 100x100 and a grid-size of 1x1 +x = y = range(100) +model = gs.Gaussian(dim=2, var=1, len_scale=10) +srf = gs.SRF(model, seed=20170519) +field = srf.structured([x, y]) +srf.transform("normal_to_arcsin") # also "arcsin" works +srf.plot() diff --git a/GSTools/source/examples/07_transformations/05_combinations.py b/GSTools/source/examples/07_transformations/05_combinations.py new file mode 100644 index 0000000000000000000000000000000000000000..1fbe367e417e9949068d531863c710a123d71afa --- /dev/null +++ b/GSTools/source/examples/07_transformations/05_combinations.py @@ -0,0 +1,41 @@ +""" +Combinations +------------ + +You can combine different transformations simply by successively applying them. + +Here, we first force the single field realization to hold the given moments, +namely mean and variance. +Then we apply the Zinn & Harvey transformation to connect the low values. +Afterwards the field is transformed to a binary field and last but not least, +we transform it to log-values. + +We can select the desired field by its name and we can define an output name +to store the field. + +If you don't specify `field` and `store` everything happens inplace. +""" + +# sphinx_gallery_thumbnail_number = 1 +import gstools as gs + +# structured field with a size of 100x100 and a grid-size of 1x1 +x = y = range(100) +model = gs.Gaussian(dim=2, var=1, len_scale=10) +srf = gs.SRF(model, mean=-9, seed=20170519) +srf.structured([x, y]) +srf.transform("force_moments", field="field", store="f_forced") +srf.transform("zinnharvey", field="f_forced", store="f_zinnharvey", conn="low") +srf.transform("binary", field="f_zinnharvey", store="f_binary") +srf.transform("lognormal", field="f_binary", store="f_result") +srf.plot(field="f_result") + +############################################################################### +# The resulting field could be interpreted as a transmissivity field, where +# the values of low permeability are the ones being the most connected +# and only two kinds of soil exist. +# +# All stored fields can be accessed and plotted by name: + +print("Max binary value:", srf.f_binary.max()) +srf.plot(field="f_zinnharvey") diff --git a/GSTools/source/examples/07_transformations/README.rst b/GSTools/source/examples/07_transformations/README.rst new file mode 100644 index 0000000000000000000000000000000000000000..d93c99307e0e7ab383f52c6955b09ab67f5fd413 --- /dev/null +++ b/GSTools/source/examples/07_transformations/README.rst @@ -0,0 +1,50 @@ +Field transformations +===================== + +The generated fields of gstools are ordinary Gaussian random fields. +In application there are several transformations to describe real world +problems in an appropriate manner. + +GStools provides a submodule :py:mod:`gstools.transform` with a range of +common transformations: + +.. currentmodule:: gstools.transform + +.. autosummary:: + binary + discrete + boxcox + zinnharvey + normal_force_moments + normal_to_lognormal + normal_to_uniform + normal_to_arcsin + normal_to_uquad + apply_function + + +All the transformations take a field class, that holds a generated field, +as input and will manipulate this field inplace or store it with a given name. + +Simply apply a transformation to a field class: + +.. code-block:: python + + import gstools as gs + ... + srf = gs.SRF(model) + srf(...) + gs.transform.normal_to_lognormal(srf) + +Or use the provided wrapper: + +.. code-block:: python + + import gstools as gs + ... + srf = gs.SRF(model) + srf(...) + srf.transform("lognormal") + +Examples +-------- diff --git a/GSTools/source/examples/08_geo_coordinates/00_field_generation.py b/GSTools/source/examples/08_geo_coordinates/00_field_generation.py new file mode 100644 index 0000000000000000000000000000000000000000..5b1a6fca9e689a49634691b8c8daf2235ed1fa30 --- /dev/null +++ b/GSTools/source/examples/08_geo_coordinates/00_field_generation.py @@ -0,0 +1,65 @@ +""" +Working with lat-lon random fields +---------------------------------- + +In this example, we demonstrate how to generate a random field on +geographical coordinates. + +First we setup a model, with ``latlon=True``, to get the associated +Yadrenko model. + +In addition, we will use a kilometer scale provided by :any:`KM_SCALE` +as ``geo_scale`` to have a meaningful length scale in km. +By default the length scale would be given in radians (:any:`RADIAN_SCALE`). +A third option is a length scale in degrees (:any:`DEGREE_SCALE`). + +To generate the field, we simply pass ``(lat, lon)`` as the position tuple +to the :any:`SRF` class. +""" + +import numpy as np + +import gstools as gs + +model = gs.Gaussian(latlon=True, len_scale=777, geo_scale=gs.KM_SCALE) + +lat = lon = range(-80, 81) +srf = gs.SRF(model, seed=1234) +field = srf.structured((lat, lon)) +srf.plot() + +############################################################################### +# This was easy as always! Now we can use this field to estimate the empirical +# variogram in order to prove, that the generated field has the correct +# geo-statistical properties. +# The :any:`vario_estimate` routine also provides a ``latlon`` switch to +# indicate, that the given field is defined on geographical variables. +# +# As we will see, everthing went well... phew! + +bin_edges = np.linspace(0, 777 * 3, 30) +bin_center, emp_vario = gs.vario_estimate( + (lat, lon), + field, + bin_edges, + latlon=True, + mesh_type="structured", + sampling_size=2000, + sampling_seed=12345, + geo_scale=gs.KM_SCALE, +) + +ax = model.plot("vario_yadrenko", x_max=max(bin_center)) +model.fit_variogram(bin_center, emp_vario, nugget=False) +model.plot("vario_yadrenko", ax=ax, label="fitted", x_max=max(bin_center)) +ax.scatter(bin_center, emp_vario, color="k") +print(model) + +############################################################################### +# .. note:: +# +# Note, that the estimated variogram coincides with the yadrenko variogram, +# which means it depends on the great-circle distance given in radians. +# +# Keep that in mind when defining bins: The range is at most +# :math:`\pi\approx 3.14`, which corresponds to the half globe. diff --git a/GSTools/source/examples/08_geo_coordinates/01_dwd_krige.py b/GSTools/source/examples/08_geo_coordinates/01_dwd_krige.py new file mode 100644 index 0000000000000000000000000000000000000000..3c17fb7e08491547c8aa03cfe3485e391c5bf91b --- /dev/null +++ b/GSTools/source/examples/08_geo_coordinates/01_dwd_krige.py @@ -0,0 +1,174 @@ +""" +Kriging geographical data +------------------------- + +In this example we are going to interpolate actual temperature data from +the German weather service `DWD `_. + +Data is retrieved utilizing the beautiful package +`wetterdienst `_, +which serves as an API for the DWD data. + +For better visualization, we also download a simple shapefile of the German +borderline with `cartopy `_. + +In order to keep the number of dependecies low, the calls of both functions +shown beneath are commented out. +""" + +# sphinx_gallery_thumbnail_number = 2 +import matplotlib.pyplot as plt +import numpy as np + +import gstools as gs + + +def get_borders_germany(): + """Download simple german shape file with cartopy.""" + import geopandas as gp # 0.8.1 + from cartopy.io import shapereader as shp_read # version 0.18.0 + + shpfile = shp_read.natural_earth("50m", "cultural", "admin_0_countries") + df = gp.read_file(shpfile) # only use the simplest polygon + poly = df.loc[df["ADMIN"] == "Germany"]["geometry"].values[0][0] + np.savetxt("de_borders.txt", list(poly.exterior.coords)) + + +def get_dwd_temperature(date="2020-06-09 12:00:00"): + """Get air temperature from german weather stations from 9.6.20 12:00.""" + from wetterdienst.dwd import observations as obs # version 0.13.0 + + settings = dict( + resolution=obs.DWDObservationResolution.HOURLY, + start_date=date, + end_date=date, + ) + sites = obs.DWDObservationStations( + parameter_set=obs.DWDObservationParameterSet.TEMPERATURE_AIR, + period=obs.DWDObservationPeriod.RECENT, + **settings, + ) + ids, lat, lon = sites.all().loc[:, ["STATION_ID", "LAT", "LON"]].values.T + observations = obs.DWDObservationData( + station_ids=ids, + parameters=obs.DWDObservationParameter.HOURLY.TEMPERATURE_AIR_200, + periods=obs.DWDObservationPeriod.RECENT, + **settings, + ) + temp = observations.all().VALUE.values + sel = np.isfinite(temp) + # select only valid temperature data + ids, lat, lon, temp = ids.astype(float)[sel], lat[sel], lon[sel], temp[sel] + head = "id, lat, lon, temp" # add a header to the file + np.savetxt("temp_obs.txt", np.array([ids, lat, lon, temp]).T, header=head) + + +############################################################################### +# If you want to download the data again, +# uncomment the two following lines. We will simply load the resulting +# files to gain the border polygon and the observed temperature along with +# the station locations given by lat-lon values. + +# get_borders_germany() +# get_dwd_temperature(date="2020-06-09 12:00:00") + +border = np.loadtxt("de_borders.txt") +ids, lat, lon, temp = np.loadtxt("temp_obs.txt").T + +############################################################################### +# First we will estimate the variogram of our temperature data. +# As the maximal bin distance we choose 900 km. + +bin_center, vario = gs.vario_estimate( + (lat, lon), temp, latlon=True, geo_scale=gs.KM_SCALE, max_dist=900 +) + +############################################################################### +# Now we can use this estimated variogram to fit a model to it. +# Here we will use a :any:`Spherical` model. We select the ``latlon`` option +# to use the `Yadrenko` variant of the model to gain a valid model for lat-lon +# coordinates and we set the ``geo_scale`` to the earth-radius. Otherwise the length +# scale would be given in radians representing the great-circle distance. +# +# We deselect the nugget from fitting and plot the result afterwards. +# +# .. note:: +# +# You need to plot the Yadrenko variogram, since the standard variogram +# still holds the ordinary routine that is not respecting the great-circle +# distance. + +model = gs.Spherical(latlon=True, geo_scale=gs.KM_SCALE) +model.fit_variogram(bin_center, vario, nugget=False) +ax = model.plot("vario_yadrenko", x_max=max(bin_center)) +ax.scatter(bin_center, vario) +print(model) + +############################################################################### +# As we see, we have a rather large correlation length of 600 km. +# +# Now we want to interpolate the data using :any:`Universal` kriging. +# In order to tinker around with the data, we will use a north-south drift +# by assuming a linear correlation with the latitude. +# This can be done as follows: + + +def north_south_drift(lat, lon): + return lat + + +uk = gs.krige.Universal( + model=model, + cond_pos=(lat, lon), + cond_val=temp, + drift_functions=north_south_drift, +) + +############################################################################### +# Now we generate the kriging field, by defining a lat-lon grid that covers +# the whole of Germany. The :any:`Krige` class provides the option to only +# krige the mean field, so one can have a glimpse at the estimated drift. + +g_lat = np.arange(47, 56.1, 0.1) +g_lon = np.arange(5, 16.1, 0.1) + +uk.set_pos((g_lat, g_lon), mesh_type="structured") +uk(return_var=False, store="temp_field") +uk(only_mean=True, store="mean_field") + +############################################################################### +# And that's it. Now let's have a look at the generated field and the input +# data along with the estimated mean: + +levels = np.linspace(5, 23, 64) +fig, ax = plt.subplots(1, 3, figsize=[10, 5], sharey=True) +sca = ax[0].scatter(lon, lat, c=temp, vmin=5, vmax=23, cmap="coolwarm") +co1 = ax[1].contourf(g_lon, g_lat, uk["temp_field"], levels, cmap="coolwarm") +co2 = ax[2].contourf(g_lon, g_lat, uk["mean_field"], levels, cmap="coolwarm") + +[ax[i].plot(border[:, 0], border[:, 1], color="k") for i in range(3)] +[ax[i].set_xlim([5, 16]) for i in range(3)] +[ax[i].set_xlabel("Lon in deg") for i in range(3)] +ax[0].set_ylabel("Lat in deg") + +ax[0].set_title("Temperature observations at 2m\nfrom DWD (2020-06-09 12:00)") +ax[1].set_title("Interpolated temperature\nwith North-South drift") +ax[2].set_title("Estimated mean drift\nfrom Universal Kriging") + +fmt = dict(orientation="horizontal", shrink=0.5, fraction=0.1, pad=0.2) +fig.colorbar(co2, ax=ax, **fmt).set_label("T in [°C]") + +############################################################################### +# To get a better impression of the estimated north-south drift, we'll take +# a look at a cross-section at a longitude of 10 degree: + +fig, ax = plt.subplots() +ax.plot(g_lat, uk["temp_field"][:, 50], label="Interpolated temperature") +ax.plot(g_lat, uk["mean_field"][:, 50], label="North-South mean drift") +ax.set_xlabel("Lat in deg") +ax.set_ylabel("T in [°C]") +ax.set_title("North-South cross-section at 10°") +ax.legend() + +############################################################################### +# Interpretion of the results is now up to you! ;-) diff --git a/GSTools/source/examples/08_geo_coordinates/README.rst b/GSTools/source/examples/08_geo_coordinates/README.rst new file mode 100644 index 0000000000000000000000000000000000000000..b0cf79d138c4d0cc0dc35f901e627ecc12c932da --- /dev/null +++ b/GSTools/source/examples/08_geo_coordinates/README.rst @@ -0,0 +1,68 @@ +Geographic Coordinates +====================== + +GSTools provides support for +`geographic coordinates `_ +given by: + +- latitude ``lat``: specifies the north–south position of a point on the Earth's surface +- longitude ``lon``: specifies the east–west position of a point on the Earth's surface + +If you want to use this feature for field generation or Kriging, you +have to set up a geographical covariance Model by setting ``latlon=True`` +in your desired model (see :any:`CovModel`): + +.. code-block:: python + + import numpy as np + import gstools as gs + + model = gs.Gaussian(latlon=True, var=2, len_scale=np.pi / 16) + +By doing so, the model will use the associated `Yadrenko` model on a sphere +(see `here `_). +The `len_scale` is given in radians to scale the arc-length. +In order to have a more meaningful length scale, one can use the ``geo_scale`` +argument: + +.. code-block:: python + + import gstools as gs + + model = gs.Gaussian(latlon=True, var=2, len_scale=500, geo_scale=gs.KM_SCALE) + +Then ``len_scale`` can be interpreted as given in km. + +A `Yadrenko` model :math:`C` is derived from a valid +isotropic covariance model in 3D :math:`C_{3D}` by the following relation: + +.. math:: + C(\zeta)=C_{3D}\left(2r \cdot \sin\left(\frac{\zeta}{2r}\right)\right) + +Where :math:`\zeta` is the +`great-circle distance `_ +and :math:`r` is the ``geo_scale``. + +.. note:: + + ``lat`` and ``lon`` are given in degree, whereas the great-circle distance + :math:`zeta` is given in units of the ``geo_scale``. + +Note, that :math:`2r \cdot \sin(\frac{\zeta}{2r})` is the +`chordal distance `_ +of two points on a sphere with radius :math:`r`, which means we simply think of the +earth surface as a sphere, that is cut out of the surrounding three dimensional space, +when using the `Yadrenko` model. + +.. note:: + + Anisotropy is not available with the geographical models, since their + geometry is not euclidean. When passing values for :any:`CovModel.anis` + or :any:`CovModel.angles`, they will be ignored. + + Since the Yadrenko model comes from a 3D model, the model dimension will + be 3 (see :any:`CovModel.dim`) but the `field_dim` will be 2 in this case + (see :any:`CovModel.field_dim`). + +Examples +-------- diff --git a/GSTools/source/examples/08_geo_coordinates/de_borders.txt b/GSTools/source/examples/08_geo_coordinates/de_borders.txt new file mode 100644 index 0000000000000000000000000000000000000000..c8cdb5a888a1c0d86151f6f96c39e12810a17314 --- /dev/null +++ b/GSTools/source/examples/08_geo_coordinates/de_borders.txt @@ -0,0 +1,492 @@ +9.524023437500005684e+00 4.752421874999999574e+01 +9.350000000000022737e+00 4.759892578124999574e+01 +9.182812500000011369e+00 4.767070312499999574e+01 +9.127539062500005684e+00 4.767070312499999574e+01 +8.881152343750017053e+00 4.765639648437499432e+01 +8.874023437500000000e+00 4.766269531249999858e+01 +8.831152343750005684e+00 4.770361328125000000e+01 +8.793066406250005684e+00 4.771655273437500000e+01 +8.770117187500005684e+00 4.770991210937499716e+01 +8.754785156250022737e+00 4.769804687499999574e+01 +8.728320312500017053e+00 4.770004882812499858e+01 +8.617871093750011369e+00 4.776611328125000000e+01 +8.572656250000022737e+00 4.777563476562500000e+01 +8.509863281250005684e+00 4.776689453124999574e+01 +8.435742187500011369e+00 4.773134765624999432e+01 +8.403417968750005684e+00 4.768779296874999574e+01 +8.413281250000011369e+00 4.766269531249999858e+01 +8.451757812500005684e+00 4.765180664062499716e+01 +8.552343750000005684e+00 4.765913085937499716e+01 +8.567089843750011369e+00 4.765190429687499574e+01 +8.570507812500011369e+00 4.763779296874999858e+01 +8.559472656250022737e+00 4.762402343750000000e+01 +8.477636718750005684e+00 4.761269531249999432e+01 +8.454003906250022737e+00 4.759619140625000000e+01 +8.430078125000022737e+00 4.759213867187499858e+01 +8.414746093750011369e+00 4.758959960937500000e+01 +8.327832031250011369e+00 4.760693359375000000e+01 +8.198242187500000000e+00 4.760693359375000000e+01 +8.093750000000000000e+00 4.757617187500000000e+01 +7.927050781250017053e+00 4.756386718749999432e+01 +7.698046875000017053e+00 4.756987304687499574e+01 +7.615625000000022737e+00 4.759272460937499716e+01 +7.565429687500000000e+00 4.760654296874999858e+01 +7.529394531250005684e+00 4.767387695312499574e+01 +7.538574218750000000e+00 4.777363281249999716e+01 +7.593261718750000000e+00 4.790566406249999432e+01 +7.608496093750005684e+00 4.800258789062499432e+01 +7.584179687500011369e+00 4.806430664062499858e+01 +7.616601562500022737e+00 4.815678710937499574e+01 +7.705664062500005684e+00 4.828002929687500000e+01 +7.765136718750000000e+00 4.841000976562499858e+01 +7.794824218750022737e+00 4.854682617187499716e+01 +7.837988281250005684e+00 4.863603515624999574e+01 +7.922753906250022737e+00 4.869853515624999574e+01 +8.124023437500000000e+00 4.887329101562500000e+01 +8.140332031250011369e+00 4.888642578124999716e+01 +8.134863281250005684e+00 4.897358398437499716e+01 +8.080664062500005684e+00 4.898588867187499574e+01 +8.001269531250017053e+00 4.901093749999999716e+01 +7.799218750000022737e+00 4.904189453124999432e+01 +7.610937500000005684e+00 4.906176757812500000e+01 +7.525488281250005684e+00 4.908637695312499716e+01 +7.450585937500022737e+00 4.915219726562499858e+01 +7.404199218750022737e+00 4.915307617187500000e+01 +7.313378906250022737e+00 4.912954101562499432e+01 +7.199902343750011369e+00 4.911362304687499858e+01 +7.117382812500011369e+00 4.912753906249999858e+01 +7.065722656250017053e+00 4.912485351562499858e+01 +7.036718750000005684e+00 4.911269531249999432e+01 +7.022167968750011369e+00 4.912343749999999432e+01 +7.001464843750000000e+00 4.917988281249999716e+01 +6.958300781250017053e+00 4.919462890624999574e+01 +6.891210937500005684e+00 4.920751953125000000e+01 +6.849511718750022737e+00 4.920195312499999574e+01 +6.820703125000022737e+00 4.917392578124999858e+01 +6.776269531250022737e+00 4.915415039062499858e+01 +6.735449218750005684e+00 4.916059570312499716e+01 +6.607617187500011369e+00 4.929086914062499858e+01 +6.574707031250000000e+00 4.931967773437499858e+01 +6.566308593750022737e+00 4.934619140625000000e+01 +6.534277343750005684e+00 4.939467773437499432e+01 +6.458105468750005684e+00 4.944287109375000000e+01 +6.382226562500022737e+00 4.945815429687499432e+01 +6.344335937500005684e+00 4.945273437499999858e+01 +6.348437500000017053e+00 4.951269531250000000e+01 +6.378320312500022737e+00 4.959960937500000000e+01 +6.406738281250000000e+00 4.964497070312499716e+01 +6.444628906250017053e+00 4.968203124999999432e+01 +6.484765625000022737e+00 4.970781249999999574e+01 +6.493750000000005684e+00 4.975439453125000000e+01 +6.487304687500000000e+00 4.979848632812499432e+01 +6.440917968750000000e+00 4.980532226562499432e+01 +6.324609375000022737e+00 4.983789062500000000e+01 +6.256054687500011369e+00 4.987216796874999858e+01 +6.204882812500017053e+00 4.991513671874999858e+01 +6.138183593750000000e+00 4.997431640624999716e+01 +6.109765625000022737e+00 5.003437499999999716e+01 +6.108300781250022737e+00 5.009423828125000000e+01 +6.116503906250017053e+00 5.012099609374999432e+01 +6.121289062500011369e+00 5.013935546874999716e+01 +6.175097656250017053e+00 5.023266601562500000e+01 +6.364453125000011369e+00 5.031616210937500000e+01 +6.343652343750022737e+00 5.040024414062499858e+01 +6.340917968750005684e+00 5.045175781249999858e+01 +6.294921875000000000e+00 5.048549804687499432e+01 +6.203027343750022737e+00 5.049912109374999858e+01 +6.178710937500000000e+00 5.052250976562499574e+01 +6.168457031250000000e+00 5.054536132812499716e+01 +6.235937500000005684e+00 5.059667968750000000e+01 +6.154492187500011369e+00 5.063725585937499574e+01 +6.119433593750017053e+00 5.067924804687499574e+01 +6.005957031250005684e+00 5.073222656249999574e+01 +5.993945312500017053e+00 5.075043945312499716e+01 +6.048437500000005684e+00 5.090488281249999858e+01 +6.006835937500000000e+00 5.094995117187500000e+01 +5.955078125000000000e+00 5.097294921874999574e+01 +5.894726562500011369e+00 5.098422851562499858e+01 +5.867187500000000000e+00 5.100566406249999574e+01 +5.857519531250005684e+00 5.103012695312499858e+01 +5.868359375000011369e+00 5.104531249999999432e+01 +5.939257812500017053e+00 5.104082031249999574e+01 +5.961035156250005684e+00 5.105668945312499574e+01 +6.129980468750005684e+00 5.114741210937499716e+01 +6.136914062500011369e+00 5.116484374999999574e+01 +6.113378906250005684e+00 5.117470703124999432e+01 +6.082421875000022737e+00 5.117998046874999574e+01 +6.074804687500005684e+00 5.119902343749999574e+01 +6.075878906250011369e+00 5.122412109375000000e+01 +6.166210937500011369e+00 5.135483398437499858e+01 +6.192871093750000000e+00 5.141059570312499716e+01 +6.198828125000005684e+00 5.144999999999999574e+01 +6.193261718750022737e+00 5.148891601562499432e+01 +6.141601562500000000e+00 5.155009765624999574e+01 +6.091113281250017053e+00 5.159892578124999574e+01 +6.089355468750000000e+00 5.163779296874999858e+01 +6.052734375000000000e+00 5.165825195312499574e+01 +5.948535156250017053e+00 5.176240234374999716e+01 +5.948730468750000000e+00 5.180268554687499716e+01 +6.007617187500017053e+00 5.183398437500000000e+01 +6.089843750000000000e+00 5.185395507812499716e+01 +6.117187500000000000e+00 5.187041015624999574e+01 +6.166503906250000000e+00 5.188076171875000142e+01 +6.297070312500011369e+00 5.185073242187500142e+01 +6.355664062500011369e+00 5.182465820312499716e+01 +6.372167968750005684e+00 5.183002929687499716e+01 +6.425000000000011369e+00 5.185839843750000000e+01 +6.517578125000000000e+00 5.185395507812499716e+01 +6.741796875000005684e+00 5.191088867187500000e+01 +6.775195312500017053e+00 5.193828124999999574e+01 +6.800390625000005684e+00 5.196738281249999858e+01 +6.802441406250011369e+00 5.198017578124999716e+01 +6.715625000000017053e+00 5.203618164062499574e+01 +6.712988281250005684e+00 5.205688476562500000e+01 +6.724511718750022737e+00 5.208022460937500142e+01 +6.749023437500000000e+00 5.209868164062499574e+01 +6.800390625000005684e+00 5.211123046875000142e+01 +6.855078125000005684e+00 5.213579101562499574e+01 +6.977246093750011369e+00 5.220551757812499716e+01 +7.019628906250005684e+00 5.226601562500000142e+01 +7.032617187500022737e+00 5.233149414062499716e+01 +7.035156250000000000e+00 5.238022460937499858e+01 +7.001855468750022737e+00 5.241899414062499574e+01 +6.968164062500022737e+00 5.244409179687500000e+01 +6.922070312500011369e+00 5.244028320312499858e+01 +6.832519531250000000e+00 5.244228515625000142e+01 +6.748828125000017053e+00 5.246401367187500142e+01 +6.702929687500017053e+00 5.249921874999999716e+01 +6.691601562500011369e+00 5.253017578125000142e+01 +6.712402343750000000e+00 5.254965820312499858e+01 +6.718750000000000000e+00 5.257358398437499858e+01 +6.705371093750017053e+00 5.259765625000000000e+01 +6.710742187500017053e+00 5.261787109374999716e+01 +6.748437500000022737e+00 5.263408203124999574e+01 +7.013183593750000000e+00 5.263354492187500000e+01 +7.033007812500017053e+00 5.265136718750000000e+01 +7.050878906250005684e+00 5.274477539062500142e+01 +7.117089843750022737e+00 5.288701171874999574e+01 +7.179492187500017053e+00 5.296621093750000142e+01 +7.189941406250000000e+00 5.299951171875000000e+01 +7.188964843750000000e+00 5.318720703124999716e+01 +7.197265625000000000e+00 5.328227539062499574e+01 +7.152050781250011369e+00 5.332695312499999574e+01 +7.053320312500005684e+00 5.337583007812499858e+01 +7.074316406250005684e+00 5.347763671874999858e+01 +7.107128906250011369e+00 5.355698242187499858e+01 +7.206445312500022737e+00 5.365454101562500000e+01 +7.285253906250005684e+00 5.368134765624999716e+01 +7.629199218750017053e+00 5.369726562500000000e+01 +8.009277343750000000e+00 5.369072265624999574e+01 +8.167089843750005684e+00 5.354340820312499716e+01 +8.108496093750005684e+00 5.346767578125000142e+01 +8.200781250000005684e+00 5.343242187499999574e+01 +8.245214843750005684e+00 5.344531250000000000e+01 +8.279003906250011369e+00 5.351118164062499716e+01 +8.301562500000017053e+00 5.358413085937500142e+01 +8.333886718750022737e+00 5.360620117187500000e+01 +8.451367187500011369e+00 5.355170898437499716e+01 +8.492675781250000000e+00 5.351435546874999716e+01 +8.495214843750005684e+00 5.339423828124999716e+01 +8.538476562500022737e+00 5.355688476562500000e+01 +8.506250000000022737e+00 5.367075195312499858e+01 +8.528417968750005684e+00 5.378110351562499858e+01 +8.575585937500022737e+00 5.383847656249999858e+01 +8.618945312500017053e+00 5.387500000000000000e+01 +8.897753906250017053e+00 5.383569335937500000e+01 +9.205566406250000000e+00 5.385595703125000000e+01 +9.321972656250011369e+00 5.381347656250000000e+01 +9.585351562500022737e+00 5.360048828125000142e+01 +9.673144531250017053e+00 5.356562499999999716e+01 +9.783984375000017053e+00 5.355463867187499716e+01 +9.631250000000022737e+00 5.360019531249999858e+01 +9.312011718750000000e+00 5.385913085937500000e+01 +9.216406250000005684e+00 5.389121093749999858e+01 +9.069628906250017053e+00 5.390092773437499574e+01 +8.978125000000005684e+00 5.392622070312499716e+01 +8.920410156250000000e+00 5.396533203125000000e+01 +8.903515625000011369e+00 5.400029296874999574e+01 +8.906640625000022737e+00 5.426079101562499574e+01 +8.851562500000000000e+00 5.429956054687500000e+01 +8.780371093750005684e+00 5.431303710937499574e+01 +8.736035156250011369e+00 5.429521484374999574e+01 +8.644921875000022737e+00 5.429497070312499574e+01 +8.625781250000017053e+00 5.435395507812499716e+01 +8.648046875000005684e+00 5.439765624999999716e+01 +8.831152343750005684e+00 5.442753906249999574e+01 +8.951855468750011369e+00 5.446757812499999574e+01 +8.957226562500011369e+00 5.453833007812500000e+01 +8.880957031250005684e+00 5.459394531249999716e+01 +8.789648437500005684e+00 5.469594726562500142e+01 +8.682324218750011369e+00 5.479184570312499858e+01 +8.670312500000022737e+00 5.490341796874999858e+01 +8.670703125000017053e+00 5.490332031250000000e+01 +8.857226562500017053e+00 5.490112304687500000e+01 +8.902929687500005684e+00 5.489692382812499716e+01 +9.185839843750017053e+00 5.484467773437499716e+01 +9.254980468750005684e+00 5.480800781250000142e+01 +9.341992187500011369e+00 5.480629882812500142e+01 +9.498730468750011369e+00 5.484042968749999858e+01 +9.615820312500005684e+00 5.485541992187499716e+01 +9.661230468750005684e+00 5.483437500000000142e+01 +9.725000000000022737e+00 5.482553710937499858e+01 +9.739746093750000000e+00 5.482553710937499858e+01 +9.745898437500017053e+00 5.480717773437499574e+01 +9.892285156250011369e+00 5.478061523437499858e+01 +9.953808593750011369e+00 5.473828125000000000e+01 +1.002216796875001137e+01 5.467392578124999858e+01 +1.002880859375000000e+01 5.458129882812500000e+01 +9.941308593750022737e+00 5.451464843750000000e+01 +9.868652343750000000e+00 5.447246093749999574e+01 +1.014345703125002274e+01 5.448842773437500142e+01 +1.017080078125002274e+01 5.445019531250000000e+01 +1.021240234375000000e+01 5.440893554687500000e+01 +1.036044921875000568e+01 5.443833007812499858e+01 +1.073154296875000568e+01 5.431625976562499858e+01 +1.095595703125002274e+01 5.437568359374999716e+01 +1.101337890625001137e+01 5.437915039062500000e+01 +1.106435546875002274e+01 5.428051757812500000e+01 +1.100859375000001705e+01 5.418115234375000000e+01 +1.081074218750001137e+01 5.407514648437499716e+01 +1.085458984375000568e+01 5.400981445312499574e+01 +1.091777343750001705e+01 5.399531249999999716e+01 +1.110429687500001705e+01 5.400917968750000142e+01 +1.139960937500001137e+01 5.394462890624999574e+01 +1.146113281250001137e+01 5.396474609375000142e+01 +1.170058593750002274e+01 5.411352539062500000e+01 +1.179628906250002274e+01 5.414545898437499716e+01 +1.211132812500000000e+01 5.416831054687499858e+01 +1.216865234375001137e+01 5.422587890624999574e+01 +1.229628906250002274e+01 5.428378906249999858e+01 +1.237851562500000568e+01 5.434702148437499858e+01 +1.257539062500001137e+01 5.446738281249999858e+01 +1.277910156250001705e+01 5.444570312500000142e+01 +1.289804687500000568e+01 5.442265624999999574e+01 +1.302861328125001705e+01 5.441103515625000142e+01 +1.314746093750000000e+01 5.428271484375000000e+01 +1.344804687500001705e+01 5.414086914062500000e+01 +1.372421875000000568e+01 5.415322265625000142e+01 +1.382226562500000000e+01 5.401904296875000000e+01 +1.386552734375001705e+01 5.385336914062499858e+01 +1.395039062500001137e+01 5.380136718749999858e+01 +1.402500000000000568e+01 5.376743164062499858e+01 +1.425000000000000000e+01 5.373188476562499716e+01 +1.425888671875000568e+01 5.372963867187500142e+01 +1.426611328125000000e+01 5.370712890624999858e+01 +1.427988281250000568e+01 5.362475585937500000e+01 +1.429873046875002274e+01 5.355644531249999574e+01 +1.441455078125000000e+01 5.328349609374999574e+01 +1.441230468750001137e+01 5.321674804687499716e+01 +1.441093750000001705e+01 5.319902343749999574e+01 +1.436855468750002274e+01 5.310556640624999858e+01 +1.429316406250001137e+01 5.302675781250000142e+01 +1.419365234375001705e+01 5.298232421875000142e+01 +1.413886718750001137e+01 5.293286132812500000e+01 +1.412861328125001137e+01 5.287822265624999574e+01 +1.425371093750001705e+01 5.278251953124999574e+01 +1.451406250000002274e+01 5.264560546874999858e+01 +1.461943359375001705e+01 5.252851562499999716e+01 +1.456972656250002274e+01 5.243110351562499716e+01 +1.455458984375002274e+01 5.235966796874999574e+01 +1.457392578125001137e+01 5.231416015624999716e+01 +1.461562500000002274e+01 5.227763671874999574e+01 +1.467988281250001137e+01 5.225000000000000000e+01 +1.470537109375001705e+01 5.220747070312499716e+01 +1.469238281250000000e+01 5.215004882812500142e+01 +1.470458984375000000e+01 5.211020507812499858e+01 +1.475253906250000568e+01 5.208183593749999574e+01 +1.474814453125000568e+01 5.207080078125000000e+01 +1.472480468750001137e+01 5.203085937499999858e+01 +1.469296875000000568e+01 5.195800781250000000e+01 +1.467490234375000568e+01 5.190483398437499574e+01 +1.460166015625000568e+01 5.183237304687499858e+01 +1.462392578125002274e+01 5.177080078124999574e+01 +1.468134765625001137e+01 5.169819335937499716e+01 +1.472490234375001705e+01 5.166171874999999858e+01 +1.473867187500002274e+01 5.162714843749999716e+01 +1.471093750000000000e+01 5.154492187500000000e+01 +1.472470703125000568e+01 5.152387695312499716e+01 +1.490595703125001137e+01 5.146333007812499716e+01 +1.493554687500000000e+01 5.143535156249999574e+01 +1.495312500000000000e+01 5.137714843749999716e+01 +1.501660156250000000e+01 5.125273437499999574e+01 +1.496386718750000000e+01 5.109511718749999432e+01 +1.491748046875000000e+01 5.100874023437499716e+01 +1.481425781250001705e+01 5.087163085937499574e+01 +1.480937500000001705e+01 5.085898437499999858e+01 +1.479746093750000568e+01 5.084233398437499574e+01 +1.476650390625002274e+01 5.081831054687499716e+01 +1.472333984375001137e+01 5.081469726562500000e+01 +1.465820312500000000e+01 5.083261718749999858e+01 +1.461357421875001705e+01 5.085556640624999858e+01 +1.462382812500001705e+01 5.091474609374999716e+01 +1.459521484375000000e+01 5.091860351562499432e+01 +1.455966796875000568e+01 5.095493164062499858e+01 +1.454570312500001705e+01 5.099394531249999574e+01 +1.450732421875000000e+01 5.100986328124999858e+01 +1.436728515625000568e+01 5.102626953124999432e+01 +1.431972656250002274e+01 5.103779296874999716e+01 +1.428320312500000000e+01 5.102949218749999716e+01 +1.425585937500000000e+01 5.100185546874999432e+01 +1.427333984375002274e+01 5.097690429687499858e+01 +1.429941406250000568e+01 5.095258789062499716e+01 +1.437705078125000568e+01 5.091406250000000000e+01 +1.436904296875002274e+01 5.089873046874999574e+01 +1.420175781250000568e+01 5.086123046874999432e+01 +1.409648437500001705e+01 5.082275390625000000e+01 +1.399843750000002274e+01 5.080112304687499858e+01 +1.389853515625000568e+01 5.076127929687499574e+01 +1.370136718750001137e+01 5.071650390624999716e+01 +1.355673828125000568e+01 5.070463867187499574e+01 +1.352656250000001137e+01 5.069282226562499716e+01 +1.347255859375002274e+01 5.061694335937500000e+01 +1.343613281250000568e+01 5.060107421875000000e+01 +1.340117187500001705e+01 5.060932617187499716e+01 +1.337460937500000568e+01 5.062172851562499432e+01 +1.334101562500001137e+01 5.061142578124999858e+01 +1.330605468750002274e+01 5.058632812499999432e+01 +1.326953125000000000e+01 5.057641601562500000e+01 +1.323769531250002274e+01 5.057675781249999858e+01 +1.318115234375000000e+01 5.051049804687500000e+01 +1.301640625000001705e+01 5.049038085937499432e+01 +1.299707031250000000e+01 5.045605468750000000e+01 +1.296679687500000000e+01 5.041621093749999716e+01 +1.294267578125001705e+01 5.040644531249999716e+01 +1.286826171875000568e+01 5.042221679687499858e+01 +1.276542968750001705e+01 5.043095703124999574e+01 +1.270644531250002274e+01 5.040913085937499716e+01 +1.263554687500001705e+01 5.039707031249999858e+01 +1.254902343750001137e+01 5.039340820312499858e+01 +1.245263671875000000e+01 5.034980468749999716e+01 +1.235859375000001137e+01 5.027324218749999574e+01 +1.230566406250000000e+01 5.020571289062499432e+01 +1.227734375000000000e+01 5.018144531249999574e+01 +1.223115234375001137e+01 5.024487304687500000e+01 +1.217480468750000000e+01 5.028837890624999574e+01 +1.213486328125000568e+01 5.031093749999999432e+01 +1.209921875000000568e+01 5.031098632812499716e+01 +1.208984375000000000e+01 5.030175781250000000e+01 +1.208974609375002274e+01 5.026855468750000000e+01 +1.212783203125002274e+01 5.021342773437499574e+01 +1.217500000000001137e+01 5.017583007812499574e+01 +1.218251953125002274e+01 5.014804687499999858e+01 +1.220781250000001705e+01 5.009750976562499858e+01 +1.227646484375000568e+01 5.004233398437499858e+01 +1.238417968750002274e+01 4.999858398437499574e+01 +1.245761718750000568e+01 4.995551757812499716e+01 +1.251201171875001705e+01 4.989580078124999574e+01 +1.251250000000001705e+01 4.987744140625000000e+01 +1.249755859375000000e+01 4.985307617187499574e+01 +1.247187500000001137e+01 4.983007812500000000e+01 +1.245019531250000000e+01 4.980014648437499858e+01 +1.239052734375002274e+01 4.973964843749999432e+01 +1.240820312500000000e+01 4.971318359374999574e+01 +1.245703125000000000e+01 4.967978515624999858e+01 +1.250029296875001705e+01 4.963969726562499574e+01 +1.255576171875000568e+01 4.957485351562499432e+01 +1.263203125000001137e+01 4.946123046874999574e+01 +1.268115234375000000e+01 4.941450195312499716e+01 +1.274785156250001705e+01 4.936621093750000000e+01 +1.281337890625002274e+01 4.932934570312500000e+01 +1.291669921875001137e+01 4.933046874999999432e+01 +1.302373046875001705e+01 4.926010742187499858e+01 +1.314052734375002274e+01 4.915834960937499432e+01 +1.322783203125001705e+01 4.911166992187499858e+01 +1.328876953125001137e+01 4.909746093749999574e+01 +1.333906250000001137e+01 4.906079101562500000e+01 +1.338369140625002274e+01 4.900810546874999574e+01 +1.340117187500001705e+01 4.897758789062499574e+01 +1.344072265625001705e+01 4.895556640625000000e+01 +1.354765625000001705e+01 4.895966796874999716e+01 +1.368496093750002274e+01 4.887670898437500000e+01 +1.376992187500002274e+01 4.881596679687499574e+01 +1.381474609375001705e+01 4.876694335937499858e+01 +1.380292968750001137e+01 4.874750976562499716e+01 +1.379746093750000568e+01 4.868642578124999432e+01 +1.379882812500000000e+01 4.862167968749999858e+01 +1.378535156250001137e+01 4.858745117187499574e+01 +1.372392578125001705e+01 4.854238281249999432e+01 +1.369218750000001705e+01 4.853276367187499574e+01 +1.367519531250002274e+01 4.852304687499999858e+01 +1.348662109375001705e+01 4.858183593749999574e+01 +1.347167968750000000e+01 4.857182617187499574e+01 +1.345986328125002274e+01 4.856455078124999858e+01 +1.340937500000001137e+01 4.839414062499999858e+01 +1.337460937500000568e+01 4.836137695312499574e+01 +1.332285156250000568e+01 4.833124999999999716e+01 +1.321523437500002274e+01 4.830190429687499432e+01 +1.314042968750001705e+01 4.828994140624999432e+01 +1.308212890625000568e+01 4.827509765624999716e+01 +1.289746093750000000e+01 4.820371093749999858e+01 +1.281425781250001705e+01 4.816083984374999716e+01 +1.276035156250000568e+01 4.810698242187499574e+01 +1.276005859375001705e+01 4.807597656249999574e+01 +1.284990234375001705e+01 4.798481445312499716e+01 +1.295351562500002274e+01 4.789062500000000000e+01 +1.295419921875000568e+01 4.780776367187499432e+01 +1.290830078125000568e+01 4.774580078124999716e+01 +1.289765625000001137e+01 4.772187499999999716e+01 +1.292812500000002274e+01 4.771284179687499716e+01 +1.298554687500001137e+01 4.770942382812499716e+01 +1.303359375000002274e+01 4.769873046875000000e+01 +1.305410156250002274e+01 4.765512695312499858e+01 +1.304794921875000568e+01 4.757915039062499574e+01 +1.303154296875001705e+01 4.750800781249999716e+01 +1.301435546875001137e+01 4.747807617187499574e+01 +1.296806640625001705e+01 4.747568359374999858e+01 +1.287890625000000000e+01 4.750644531249999858e+01 +1.280937500000001705e+01 4.754218749999999716e+01 +1.278281250000000568e+01 4.756416015624999716e+01 +1.278115234375002274e+01 4.759042968749999858e+01 +1.279619140625001705e+01 4.760703124999999858e+01 +1.277138671875002274e+01 4.763940429687500000e+01 +1.268583984375001705e+01 4.766933593749999432e+01 +1.259423828125000000e+01 4.765629882812499574e+01 +1.252656250000001137e+01 4.763613281249999432e+01 +1.248291015625000000e+01 4.763730468749999858e+01 +1.243574218750001137e+01 4.766611328124999858e+01 +1.236318359375002274e+01 4.768818359374999716e+01 +1.226835937500001705e+01 4.770273437499999858e+01 +1.220927734375001705e+01 4.771826171875000000e+01 +1.219687500000000568e+01 4.770908203124999858e+01 +1.220380859375001137e+01 4.764672851562500000e+01 +1.218564453125000568e+01 4.761953124999999432e+01 +1.171679687500000000e+01 4.758349609375000000e+01 +1.157392578125001137e+01 4.754975585937499716e+01 +1.146992187500001137e+01 4.750610351562500000e+01 +1.139296875000002274e+01 4.748715820312499858e+01 +1.137412109375000568e+01 4.746025390624999574e+01 +1.129794921875000568e+01 4.742490234374999858e+01 +1.121191406250000000e+01 4.741362304687499574e+01 +1.119121093750001705e+01 4.742519531249999432e+01 +1.113603515625001705e+01 4.740888671874999716e+01 +1.104199218750000000e+01 4.739311523437499574e+01 +1.098085937500002274e+01 4.739814453124999716e+01 +1.095214843750000000e+01 4.742670898437499716e+01 +1.089394531250002274e+01 4.747045898437500000e+01 +1.087060546875000000e+01 4.750078124999999574e+01 +1.087304687500000000e+01 4.752021484374999716e+01 +1.074160156250002274e+01 4.752412109374999716e+01 +1.065869140625000000e+01 4.754721679687499858e+01 +1.048281250000002274e+01 4.754179687499999574e+01 +1.043945312500000000e+01 4.755156249999999574e+01 +1.043037109375001137e+01 4.754106445312499574e+01 +1.040390625000000568e+01 4.741699218750000000e+01 +1.036914062500000000e+01 4.736606445312499858e+01 +1.031279296875001705e+01 4.731342773437499716e+01 +1.024062500000002274e+01 4.728413085937499716e+01 +1.018300781250002274e+01 4.727880859375000000e+01 +1.018574218750001137e+01 4.731718749999999574e+01 +1.020029296875000568e+01 4.736342773437499432e+01 +1.015878906250000568e+01 4.737426757812500000e+01 +1.009648437500001705e+01 4.737958984374999716e+01 +1.006630859375002274e+01 4.739335937499999574e+01 +1.007421875000000000e+01 4.742851562499999574e+01 +1.005986328125001705e+01 4.744907226562499858e+01 +1.003408203125002274e+01 4.747358398437499716e+01 +9.971582031250022737e+00 4.750532226562499716e+01 +9.839160156250017053e+00 4.755229492187499574e+01 +9.748925781250022737e+00 4.757553710937499858e+01 +9.715136718750017053e+00 4.755078125000000000e+01 +9.650585937500011369e+00 4.752587890625000000e+01 +9.548925781250005684e+00 4.753403320312499858e+01 +9.524023437500005684e+00 4.752421874999999574e+01 diff --git a/GSTools/source/examples/08_geo_coordinates/temp_obs.txt b/GSTools/source/examples/08_geo_coordinates/temp_obs.txt new file mode 100644 index 0000000000000000000000000000000000000000..aa8e60fc87d746e1f3c420919fcc20c5a589f995 --- /dev/null +++ b/GSTools/source/examples/08_geo_coordinates/temp_obs.txt @@ -0,0 +1,494 @@ +# id, lat, lon, temp +4.400000000000000000e+01 5.293359999999999843e+01 8.237000000000000099e+00 1.569999999999999929e+01 +7.300000000000000000e+01 4.861590000000000344e+01 1.305059999999999931e+01 1.390000000000000036e+01 +7.800000000000000000e+01 5.248530000000000229e+01 7.912600000000000300e+00 1.509999999999999964e+01 +9.100000000000000000e+01 5.074459999999999837e+01 9.345000000000000639e+00 1.700000000000000000e+01 +9.600000000000000000e+01 5.294369999999999976e+01 1.285180000000000078e+01 2.189999999999999858e+01 +1.020000000000000000e+02 5.386330000000000240e+01 8.127499999999999503e+00 1.190000000000000036e+01 +1.250000000000000000e+02 4.783420000000000272e+01 1.086669999999999980e+01 1.140000000000000036e+01 +1.310000000000000000e+02 5.108809999999999718e+01 1.293260000000000076e+01 1.719999999999999929e+01 +1.420000000000000000e+02 4.840599999999999881e+01 1.131170000000000009e+01 1.290000000000000036e+01 +1.500000000000000000e+02 4.972729999999999961e+01 8.116400000000000503e+00 1.719999999999999929e+01 +1.510000000000000000e+02 4.946909999999999741e+01 1.185459999999999958e+01 1.340000000000000036e+01 +1.540000000000000000e+02 4.801970000000000027e+01 1.229250000000000043e+01 1.390000000000000036e+01 +1.610000000000000000e+02 5.042369999999999663e+01 7.420200000000000351e+00 1.810000000000000142e+01 +1.640000000000000000e+02 5.303159999999999741e+01 1.399080000000000013e+01 2.130000000000000071e+01 +1.670000000000000000e+02 5.384120000000000061e+01 1.368459999999999965e+01 2.130000000000000071e+01 +1.830000000000000000e+02 5.467920000000000158e+01 1.343430000000000035e+01 1.739999999999999858e+01 +1.910000000000000000e+02 4.996940000000000026e+01 9.911400000000000432e+00 1.860000000000000142e+01 +1.980000000000000000e+02 5.137449999999999761e+01 1.129199999999999982e+01 2.019999999999999929e+01 +2.170000000000000000e+02 4.787740000000000151e+01 1.136430000000000007e+01 1.269999999999999929e+01 +2.220000000000000000e+02 5.059080000000000155e+01 1.271390000000000065e+01 1.580000000000000071e+01 +2.320000000000000000e+02 4.842530000000000001e+01 1.094170000000000087e+01 1.340000000000000036e+01 +2.570000000000000000e+02 4.872699999999999676e+01 8.245699999999999363e+00 1.359999999999999964e+01 +2.590000000000000000e+02 4.780639999999999645e+01 7.638700000000000045e+00 1.440000000000000036e+01 +2.820000000000000000e+02 4.987429999999999808e+01 1.092060000000000031e+01 1.580000000000000071e+01 +2.940000000000000000e+02 5.231989999999999696e+01 9.429999999999999716e+00 2.150000000000000000e+01 +2.980000000000000000e+02 5.434060000000000201e+01 1.271080000000000076e+01 1.769999999999999929e+01 +3.030000000000000000e+02 5.206139999999999901e+01 1.349959999999999916e+01 2.119999999999999929e+01 +3.140000000000000000e+02 5.116040000000000276e+01 1.450420000000000087e+01 2.039999999999999858e+01 +3.200000000000000000e+02 4.996670000000000300e+01 1.151970000000000027e+01 1.469999999999999929e+01 +3.300000000000000000e+02 4.956170000000000186e+01 8.967299999999999827e+00 1.409999999999999964e+01 +3.420000000000000000e+02 5.231700000000000017e+01 8.169399999999999551e+00 1.639999999999999858e+01 +3.680000000000000000e+02 5.281519999999999726e+01 9.924799999999999400e+00 1.919999999999999929e+01 +3.770000000000000000e+02 4.910699999999999932e+01 7.996699999999999697e+00 1.669999999999999929e+01 +3.790000000000000000e+02 5.090740000000000265e+01 1.126650000000000063e+01 1.810000000000000142e+01 +3.900000000000000000e+02 5.098369999999999891e+01 8.368299999999999628e+00 1.480000000000000071e+01 +4.000000000000000000e+02 5.263089999999999691e+01 1.350220000000000020e+01 2.250000000000000000e+01 +4.030000000000000000e+02 5.245369999999999777e+01 1.330170000000000030e+01 1.919999999999999929e+01 +4.100000000000000000e+02 5.240400000000000347e+01 1.373090000000000011e+01 2.080000000000000071e+01 +4.200000000000000000e+02 5.254469999999999885e+01 1.355979999999999919e+01 2.169999999999999929e+01 +4.270000000000000000e+02 5.238069999999999737e+01 1.353059999999999974e+01 2.250000000000000000e+01 +4.300000000000000000e+02 5.256439999999999912e+01 1.330879999999999974e+01 2.060000000000000142e+01 +4.330000000000000000e+02 5.246750000000000114e+01 1.340210000000000079e+01 2.060000000000000142e+01 +4.450000000000000000e+02 5.182180000000000319e+01 1.171100000000000030e+01 2.200000000000000000e+01 +4.600000000000000000e+02 4.926409999999999911e+01 6.686799999999999855e+00 1.519999999999999929e+01 +5.350000000000000000e+02 5.003719999999999857e+01 7.307900000000000063e+00 1.769999999999999929e+01 +5.910000000000000000e+02 5.339110000000000156e+01 1.068779999999999930e+01 1.889999999999999858e+01 +5.960000000000000000e+02 5.400280000000000058e+01 1.119079999999999941e+01 1.630000000000000071e+01 +6.030000000000000000e+02 5.072930000000000206e+01 7.203999999999999737e+00 1.760000000000000142e+01 +6.170000000000000000e+02 5.187299999999999756e+01 6.886300000000000310e+00 1.559999999999999964e+01 +6.560000000000000000e+02 5.172339999999999804e+01 1.060210000000000008e+01 1.580000000000000071e+01 +6.620000000000000000e+02 5.229149999999999920e+01 1.044640000000000057e+01 1.939999999999999858e+01 +6.910000000000000000e+02 5.304500000000000171e+01 8.797900000000000276e+00 1.650000000000000000e+01 +7.010000000000000000e+02 5.353320000000000078e+01 8.576100000000000279e+00 1.380000000000000071e+01 +7.040000000000000000e+02 5.344509999999999650e+01 9.138999999999999346e+00 1.610000000000000142e+01 +7.220000000000000000e+02 5.179860000000000042e+01 1.061829999999999963e+01 1.009999999999999964e+01 +7.550000000000000000e+02 4.951820000000000022e+01 9.321300000000000807e+00 1.600000000000000000e+01 +7.570000000000000000e+02 4.796249999999999858e+01 7.998300000000000409e+00 1.280000000000000071e+01 +7.600000000000000000e+02 5.336290000000000333e+01 9.943500000000000227e+00 1.750000000000000000e+01 +7.660000000000000000e+02 5.017459999999999809e+01 7.059499999999999886e+00 1.680000000000000071e+01 +7.690000000000000000e+02 5.228170000000000073e+01 9.088900000000000645e+00 1.889999999999999858e+01 +8.170000000000000000e+02 5.103059999999999974e+01 8.814600000000000435e+00 1.850000000000000000e+01 +8.400000000000000000e+02 5.043130000000000024e+01 1.261139999999999972e+01 1.080000000000000071e+01 +8.500000000000000000e+02 5.259590000000000032e+01 1.002960000000000029e+01 1.939999999999999858e+01 +8.530000000000000000e+02 5.079129999999999967e+01 1.287199999999999989e+01 1.710000000000000142e+01 +8.560000000000000000e+02 4.788430000000000319e+01 1.254039999999999999e+01 1.330000000000000071e+01 +8.670000000000000000e+02 5.030660000000000309e+01 1.096790000000000020e+01 1.739999999999999858e+01 +8.800000000000000000e+02 5.177600000000000335e+01 1.431680000000000064e+01 1.989999999999999858e+01 +8.910000000000000000e+02 5.387129999999999797e+01 8.705799999999999983e+00 1.469999999999999929e+01 +8.960000000000000000e+02 5.107780000000000342e+01 1.086190000000000033e+01 1.869999999999999929e+01 +9.170000000000000000e+02 4.988089999999999691e+01 8.677899999999999281e+00 1.639999999999999858e+01 +9.530000000000000000e+02 4.976189999999999714e+01 7.054199999999999804e+00 1.660000000000000142e+01 +9.540000000000000000e+02 5.417960000000000065e+01 7.458700000000000330e+00 1.250000000000000000e+01 +9.630000000000000000e+02 5.258809999999999718e+01 8.342399999999999594e+00 1.660000000000000142e+01 +9.790000000000000000e+02 5.073640000000000327e+01 8.267200000000000770e+00 2.060000000000000142e+01 +9.830000000000000000e+02 4.855619999999999692e+01 1.055990000000000073e+01 1.350000000000000000e+01 +9.910000000000000000e+02 5.091159999999999997e+01 1.370870000000000033e+01 1.869999999999999929e+01 +1.001000000000000000e+03 5.164509999999999934e+01 1.357469999999999999e+01 2.110000000000000142e+01 +1.048000000000000000e+03 5.112800000000000011e+01 1.375430000000000064e+01 1.869999999999999929e+01 +1.050000000000000000e+03 5.102210000000000178e+01 1.384699999999999953e+01 2.000000000000000000e+01 +1.051000000000000000e+03 5.102479999999999905e+01 1.377500000000000036e+01 1.980000000000000071e+01 +1.052000000000000000e+03 5.221739999999999782e+01 1.216409999999999947e+01 2.150000000000000000e+01 +1.072000000000000000e+03 4.947189999999999799e+01 8.192899999999999849e+00 1.669999999999999929e+01 +1.078000000000000000e+03 5.129599999999999937e+01 6.768600000000000172e+00 1.660000000000000142e+01 +1.103000000000000000e+03 4.810029999999999717e+01 1.198719999999999963e+01 1.340000000000000036e+01 +1.107000000000000000e+03 4.985199999999999676e+01 1.049910000000000032e+01 1.559999999999999964e+01 +1.161000000000000000e+03 4.887769999999999726e+01 1.123489999999999966e+01 1.450000000000000000e+01 +1.197000000000000000e+03 4.898949999999999960e+01 1.013119999999999976e+01 1.459999999999999964e+01 +1.200000000000000000e+03 5.406909999999999883e+01 9.010500000000000398e+00 1.519999999999999929e+01 +1.207000000000000000e+03 5.027049999999999841e+01 1.227420000000000044e+01 1.450000000000000000e+01 +1.214000000000000000e+03 4.820120000000000005e+01 8.108800000000000452e+00 1.250000000000000000e+01 +1.224000000000000000e+03 4.813779999999999859e+01 7.835099999999999731e+00 1.400000000000000000e+01 +1.228000000000000000e+03 5.416510000000000247e+01 6.346000000000000085e+00 1.180000000000000071e+01 +1.246000000000000000e+03 5.184179999999999922e+01 8.060700000000000642e+00 1.819999999999999929e+01 +1.262000000000000000e+03 4.834770000000000323e+01 1.181339999999999968e+01 1.350000000000000000e+01 +1.266000000000000000e+03 5.429919999999999902e+01 9.316200000000000259e+00 1.559999999999999964e+01 +1.270000000000000000e+03 5.098290000000000077e+01 1.096080000000000076e+01 1.639999999999999858e+01 +1.279000000000000000e+03 4.964970000000000283e+01 1.100740000000000052e+01 1.480000000000000071e+01 +1.297000000000000000e+03 5.120409999999999684e+01 1.001379999999999981e+01 1.639999999999999858e+01 +1.300000000000000000e+03 5.125399999999999778e+01 8.156499999999999417e+00 1.309999999999999964e+01 +1.303000000000000000e+03 5.140409999999999968e+01 6.967699999999999783e+00 1.559999999999999964e+01 +1.327000000000000000e+03 5.071189999999999998e+01 6.790499999999999758e+00 1.440000000000000036e+01 +1.332000000000000000e+03 4.848319999999999652e+01 1.272409999999999997e+01 1.290000000000000036e+01 +1.339000000000000000e+03 5.291570000000000107e+01 1.018849999999999945e+01 1.869999999999999929e+01 +1.346000000000000000e+03 4.787489999999999668e+01 8.003800000000000026e+00 4.700000000000000178e+00 +1.357000000000000000e+03 4.998069999999999879e+01 1.183760000000000012e+01 1.340000000000000036e+01 +1.358000000000000000e+03 5.042830000000000013e+01 1.295350000000000001e+01 9.400000000000000355e+00 +1.411000000000000000e+03 5.053090000000000259e+01 1.004800000000000004e+01 1.300000000000000000e+01 +1.420000000000000000e+03 5.002590000000000003e+01 8.521300000000000097e+00 1.800000000000000000e+01 +1.424000000000000000e+03 5.012689999999999912e+01 8.669399999999999551e+00 1.819999999999999929e+01 +1.443000000000000000e+03 4.802320000000000277e+01 7.834299999999999820e+00 1.380000000000000071e+01 +1.451000000000000000e+03 5.382770000000000010e+01 9.249299999999999855e+00 1.619999999999999929e+01 +1.468000000000000000e+03 4.845380000000000109e+01 8.409000000000000696e+00 9.599999999999999645e+00 +1.503000000000000000e+03 5.306430000000000291e+01 7.902199999999999669e+00 1.559999999999999964e+01 +1.504000000000000000e+03 5.111899999999999977e+01 9.279899999999999594e+00 1.869999999999999929e+01 +1.526000000000000000e+03 5.056680000000000064e+01 9.653299999999999770e+00 1.819999999999999929e+01 +1.544000000000000000e+03 5.251290000000000191e+01 1.139409999999999989e+01 2.100000000000000000e+01 +1.550000000000000000e+03 4.748299999999999699e+01 1.106209999999999916e+01 1.440000000000000036e+01 +1.580000000000000000e+03 4.998590000000000089e+01 7.954799999999999649e+00 1.989999999999999858e+01 +1.584000000000000000e+03 4.792419999999999902e+01 8.647299999999999542e+00 1.130000000000000071e+01 +1.587000000000000000e+03 4.894809999999999661e+01 1.142890000000000050e+01 1.309999999999999964e+01 +1.590000000000000000e+03 5.149419999999999931e+01 6.246299999999999741e+00 1.680000000000000071e+01 +1.602000000000000000e+03 4.843299999999999983e+01 7.993000000000000327e+00 1.450000000000000000e+01 +1.605000000000000000e+03 5.238750000000000284e+01 1.216009999999999991e+01 2.230000000000000071e+01 +1.612000000000000000e+03 5.088130000000000308e+01 1.212889999999999979e+01 1.830000000000000071e+01 +1.639000000000000000e+03 5.060170000000000101e+01 8.643900000000000361e+00 1.760000000000000142e+01 +1.645000000000000000e+03 5.096560000000000201e+01 9.050000000000000711e+00 1.930000000000000071e+01 +1.666000000000000000e+03 5.482730000000000103e+01 9.505800000000000693e+00 1.469999999999999929e+01 +1.684000000000000000e+03 5.116219999999999857e+01 1.495059999999999967e+01 1.869999999999999929e+01 +1.691000000000000000e+03 5.150019999999999953e+01 9.950699999999999434e+00 1.580000000000000071e+01 +1.694000000000000000e+03 5.360600000000000165e+01 1.210330000000000084e+01 1.960000000000000142e+01 +1.721000000000000000e+03 4.966400000000000148e+01 1.122390000000000043e+01 1.259999999999999964e+01 +1.735000000000000000e+03 4.878940000000000055e+01 1.362899999999999956e+01 1.280000000000000071e+01 +1.736000000000000000e+03 5.357309999999999661e+01 1.067970000000000041e+01 1.800000000000000000e+01 +1.757000000000000000e+03 5.409669999999999845e+01 1.340559999999999974e+01 1.919999999999999929e+01 +1.759000000000000000e+03 5.424369999999999692e+01 1.391019999999999968e+01 1.850000000000000000e+01 +1.766000000000000000e+03 5.213439999999999941e+01 7.696900000000000297e+00 1.610000000000000142e+01 +1.832000000000000000e+03 4.911290000000000333e+01 1.313380000000000081e+01 7.000000000000000000e+00 +1.863000000000000000e+03 5.026670000000000016e+01 9.185399999999999565e+00 1.730000000000000071e+01 +1.869000000000000000e+03 5.331530000000000058e+01 1.393379999999999974e+01 2.019999999999999929e+01 +1.886000000000000000e+03 4.848780000000000001e+01 1.026079999999999970e+01 1.290000000000000036e+01 +1.964000000000000000e+03 4.994449999999999790e+01 6.382100000000000328e+00 1.730000000000000071e+01 +1.975000000000000000e+03 5.363320000000000221e+01 9.988099999999999312e+00 1.739999999999999858e+01 +1.981000000000000000e+03 5.347769999999999868e+01 9.895699999999999719e+00 1.860000000000000142e+01 +2.014000000000000000e+03 5.246439999999999770e+01 9.677899999999999281e+00 2.039999999999999858e+01 +2.023000000000000000e+03 4.879180000000000206e+01 1.070620000000000083e+01 1.390000000000000036e+01 +2.039000000000000000e+03 5.190019999999999811e+01 1.056990000000000052e+01 1.789999999999999858e+01 +2.044000000000000000e+03 5.165200000000000102e+01 1.113669999999999938e+01 1.750000000000000000e+01 +2.074000000000000000e+03 4.837519999999999953e+01 8.980000000000000426e+00 1.130000000000000071e+01 +2.110000000000000000e+03 5.104110000000000014e+01 6.104199999999999626e+00 1.490000000000000036e+01 +2.115000000000000000e+03 5.417499999999999716e+01 7.892000000000000348e+00 1.400000000000000000e+01 +2.171000000000000000e+03 5.085199999999999676e+01 9.737700000000000244e+00 1.869999999999999929e+01 +2.174000000000000000e+03 5.162550000000000239e+01 1.036950000000000038e+01 1.619999999999999929e+01 +2.201000000000000000e+03 5.457500000000000284e+01 1.310440000000000005e+01 1.680000000000000071e+01 +2.211000000000000000e+03 5.073709999999999809e+01 7.652800000000000047e+00 1.430000000000000071e+01 +2.252000000000000000e+03 5.089900000000000091e+01 1.474569999999999936e+01 1.880000000000000071e+01 +2.261000000000000000e+03 5.031230000000000047e+01 1.187599999999999945e+01 1.590000000000000036e+01 +2.290000000000000000e+03 4.780089999999999861e+01 1.101079999999999970e+01 9.800000000000000711e+00 +2.303000000000000000e+03 5.431459999999999866e+01 9.538999999999999702e+00 1.630000000000000071e+01 +2.306000000000000000e+03 5.431940000000000168e+01 1.067319999999999958e+01 1.480000000000000071e+01 +2.315000000000000000e+03 5.176570000000000249e+01 1.316660000000000075e+01 1.930000000000000071e+01 +2.319000000000000000e+03 4.788230000000000075e+01 1.169609999999999950e+01 1.309999999999999964e+01 +2.323000000000000000e+03 5.185289999999999822e+01 9.495300000000000296e+00 1.960000000000000142e+01 +2.362000000000000000e+03 5.056510000000000105e+01 7.484300000000000175e+00 1.650000000000000000e+01 +2.385000000000000000e+03 4.969270000000000209e+01 7.326399999999999579e+00 1.630000000000000071e+01 +2.410000000000000000e+03 4.871119999999999806e+01 1.153619999999999912e+01 1.350000000000000000e+01 +2.429000000000000000e+03 5.398969999999999914e+01 9.569599999999999440e+00 1.689999999999999858e+01 +2.437000000000000000e+03 5.445700000000000074e+01 9.520300000000000651e+00 1.580000000000000071e+01 +2.444000000000000000e+03 5.092510000000000048e+01 1.158300000000000018e+01 2.019999999999999929e+01 +2.480000000000000000e+03 5.006430000000000291e+01 8.993000000000000327e+00 1.810000000000000142e+01 +2.483000000000000000e+03 5.118030000000000257e+01 8.489100000000000534e+00 1.380000000000000071e+01 +2.485000000000000000e+03 4.891700000000000159e+01 9.687099999999999156e+00 1.380000000000000071e+01 +2.486000000000000000e+03 4.942620000000000147e+01 7.755700000000000038e+00 1.669999999999999929e+01 +2.497000000000000000e+03 5.050139999999999674e+01 6.526399999999999757e+00 1.269999999999999929e+01 +2.559000000000000000e+03 4.772330000000000183e+01 1.033479999999999954e+01 1.240000000000000036e+01 +2.564000000000000000e+03 5.437760000000000105e+01 1.014240000000000030e+01 1.530000000000000071e+01 +2.575000000000000000e+03 4.918039999999999878e+01 9.980000000000000426e+00 1.469999999999999929e+01 +2.578000000000000000e+03 5.399949999999999761e+01 1.143410000000000082e+01 1.639999999999999858e+01 +2.597000000000000000e+03 5.022399999999999665e+01 1.007920000000000016e+01 1.730000000000000071e+01 +2.600000000000000000e+03 4.973629999999999995e+01 1.017810000000000059e+01 1.769999999999999929e+01 +2.601000000000000000e+03 5.022180000000000177e+01 8.446899999999999409e+00 1.259999999999999964e+01 +2.618000000000000000e+03 5.084579999999999700e+01 1.048029999999999973e+01 1.409999999999999964e+01 +2.627000000000000000e+03 5.155539999999999878e+01 1.388449999999999918e+01 2.030000000000000071e+01 +2.629000000000000000e+03 5.176120000000000232e+01 6.095399999999999707e+00 1.580000000000000071e+01 +2.638000000000000000e+03 4.810540000000000305e+01 8.754799999999999471e+00 9.000000000000000000e+00 +2.641000000000000000e+03 5.151850000000000307e+01 1.290649999999999942e+01 2.000000000000000000e+01 +2.667000000000000000e+03 5.086460000000000292e+01 7.157499999999999751e+00 1.830000000000000071e+01 +2.680000000000000000e+03 5.028399999999999892e+01 1.044560000000000066e+01 1.800000000000000000e+01 +2.700000000000000000e+03 4.883019999999999783e+01 1.148719999999999963e+01 1.380000000000000071e+01 +2.704000000000000000e+03 5.175110000000000099e+01 1.200939999999999941e+01 2.069999999999999929e+01 +2.708000000000000000e+03 4.766519999999999868e+01 1.108050000000000068e+01 1.269999999999999929e+01 +2.712000000000000000e+03 4.769519999999999982e+01 9.130699999999999150e+00 1.450000000000000000e+01 +2.750000000000000000e+03 5.025229999999999819e+01 1.132089999999999996e+01 1.719999999999999929e+01 +2.773000000000000000e+03 4.942830000000000013e+01 1.190160000000000018e+01 1.269999999999999929e+01 +2.794000000000000000e+03 5.293630000000000280e+01 1.240930000000000000e+01 2.150000000000000000e+01 +2.796000000000000000e+03 5.391559999999999775e+01 1.227899999999999991e+01 1.880000000000000071e+01 +2.812000000000000000e+03 4.836469999999999914e+01 7.828000000000000291e+00 1.450000000000000000e+01 +2.814000000000000000e+03 4.851209999999999667e+01 9.764499999999999957e+00 1.119999999999999929e+01 +2.856000000000000000e+03 5.191729999999999734e+01 1.308779999999999966e+01 1.900000000000000000e+01 +2.878000000000000000e+03 5.139090000000000202e+01 1.187860000000000049e+01 1.900000000000000000e+01 +2.886000000000000000e+03 4.821759999999999735e+01 9.909700000000000841e+00 1.200000000000000000e+01 +2.905000000000000000e+03 4.818489999999999895e+01 1.085069999999999979e+01 1.230000000000000071e+01 +2.907000000000000000e+03 5.479030000000000200e+01 8.951399999999999579e+00 1.340000000000000036e+01 +2.925000000000000000e+03 5.139330000000000354e+01 1.031230000000000047e+01 1.739999999999999858e+01 +2.928000000000000000e+03 5.131510000000000105e+01 1.244619999999999926e+01 2.110000000000000142e+01 +2.932000000000000000e+03 5.143480000000000274e+01 1.223959999999999937e+01 2.030000000000000071e+01 +2.947000000000000000e+03 5.113329999999999842e+01 8.034800000000000608e+00 1.540000000000000036e+01 +2.951000000000000000e+03 5.310070000000000334e+01 1.148639999999999972e+01 1.989999999999999858e+01 +2.953000000000000000e+03 4.785969999999999658e+01 8.230800000000000338e+00 9.599999999999999645e+00 +2.961000000000000000e+03 5.449960000000000093e+01 1.027369999999999983e+01 1.380000000000000071e+01 +2.968000000000000000e+03 5.098940000000000339e+01 6.977699999999999569e+00 1.730000000000000071e+01 +2.985000000000000000e+03 5.093829999999999814e+01 1.420930000000000071e+01 1.939999999999999858e+01 +3.015000000000000000e+03 5.220850000000000080e+01 1.411800000000000033e+01 2.010000000000000142e+01 +3.028000000000000000e+03 5.178540000000000276e+01 8.838800000000000878e+00 1.780000000000000071e+01 +3.031000000000000000e+03 5.163360000000000127e+01 8.394500000000000739e+00 1.900000000000000000e+01 +3.032000000000000000e+03 5.501100000000000279e+01 8.412499999999999645e+00 1.340000000000000036e+01 +3.034000000000000000e+03 5.045049999999999812e+01 1.163499999999999979e+01 1.639999999999999858e+01 +3.042000000000000000e+03 5.056170000000000186e+01 8.238599999999999923e+00 1.930000000000000071e+01 +3.083000000000000000e+03 5.192669999999999675e+01 1.387969999999999970e+01 2.119999999999999929e+01 +3.086000000000000000e+03 5.380250000000000199e+01 1.069890000000000008e+01 1.789999999999999858e+01 +3.093000000000000000e+03 5.297240000000000038e+01 1.113739999999999952e+01 1.860000000000000142e+01 +3.098000000000000000e+03 5.124519999999999698e+01 7.642500000000000071e+00 1.530000000000000071e+01 +3.126000000000000000e+03 5.210289999999999822e+01 1.158270000000000088e+01 2.050000000000000000e+01 +3.137000000000000000e+03 4.996560000000000201e+01 8.213900000000000645e+00 1.689999999999999858e+01 +3.147000000000000000e+03 4.877250000000000085e+01 1.221790000000000020e+01 1.330000000000000071e+01 +3.155000000000000000e+03 5.010150000000000148e+01 6.800900000000000389e+00 1.610000000000000142e+01 +3.158000000000000000e+03 5.254679999999999751e+01 1.454519999999999946e+01 2.089999999999999858e+01 +3.164000000000000000e+03 5.084920000000000329e+01 8.774599999999999511e+00 1.960000000000000142e+01 +3.166000000000000000e+03 5.065100000000000335e+01 1.314690000000000047e+01 1.450000000000000000e+01 +3.167000000000000000e+03 5.066210000000000235e+01 7.960300000000000153e+00 1.559999999999999964e+01 +3.196000000000000000e+03 5.332229999999999848e+01 1.193190000000000062e+01 2.180000000000000071e+01 +3.204000000000000000e+03 5.073349999999999937e+01 1.088150000000000084e+01 1.660000000000000142e+01 +3.226000000000000000e+03 5.172590000000000288e+01 1.151089999999999947e+01 2.050000000000000000e+01 +3.231000000000000000e+03 5.056119999999999948e+01 1.037710000000000043e+01 1.540000000000000036e+01 +3.234000000000000000e+03 5.112939999999999685e+01 1.343280000000000030e+01 1.880000000000000071e+01 +3.244000000000000000e+03 4.798199999999999932e+01 1.013840000000000074e+01 1.200000000000000000e+01 +3.254000000000000000e+03 5.271560000000000201e+01 7.317599999999999660e+00 1.610000000000000142e+01 +3.257000000000000000e+03 4.947729999999999961e+01 9.762199999999999989e+00 1.669999999999999929e+01 +3.268000000000000000e+03 4.816940000000000310e+01 8.943300000000000693e+00 9.500000000000000000e+00 +3.271000000000000000e+03 4.885479999999999734e+01 1.291890000000000072e+01 1.469999999999999929e+01 +3.278000000000000000e+03 4.853770000000000095e+01 9.273400000000000531e+00 1.240000000000000036e+01 +3.284000000000000000e+03 4.966910000000000025e+01 9.008499999999999730e+00 1.630000000000000071e+01 +3.287000000000000000e+03 4.971759999999999735e+01 9.099700000000000344e+00 1.490000000000000036e+01 +3.289000000000000000e+03 5.072809999999999775e+01 1.178379999999999939e+01 1.789999999999999858e+01 +3.307000000000000000e+03 4.747789999999999822e+01 1.126529999999999987e+01 1.250000000000000000e+01 +3.319000000000000000e+03 4.976440000000000197e+01 9.253000000000000114e+00 1.630000000000000071e+01 +3.340000000000000000e+03 5.043829999999999814e+01 7.806099999999999817e+00 1.789999999999999858e+01 +3.362000000000000000e+03 4.897209999999999752e+01 8.873400000000000176e+00 1.390000000000000036e+01 +3.366000000000000000e+03 4.827900000000000347e+01 1.250239999999999974e+01 1.400000000000000000e+01 +3.376000000000000000e+03 5.251760000000000161e+01 1.412320000000000064e+01 2.069999999999999929e+01 +3.379000000000000000e+03 4.816320000000000334e+01 1.154289999999999949e+01 1.390000000000000036e+01 +3.402000000000000000e+03 4.838510000000000133e+01 9.483700000000000685e+00 1.059999999999999964e+01 +3.426000000000000000e+03 5.156600000000000250e+01 1.470079999999999920e+01 1.989999999999999858e+01 +3.442000000000000000e+03 5.035739999999999839e+01 8.750600000000000378e+00 1.610000000000000142e+01 +3.484000000000000000e+03 4.870859999999999701e+01 1.121470000000000056e+01 1.580000000000000071e+01 +3.485000000000000000e+03 4.831150000000000233e+01 1.037729999999999997e+01 1.340000000000000036e+01 +3.490000000000000000e+03 5.053459999999999752e+01 7.085300000000000153e+00 1.730000000000000071e+01 +3.509000000000000000e+03 5.310199999999999676e+01 1.304209999999999958e+01 2.069999999999999929e+01 +3.513000000000000000e+03 5.050019999999999953e+01 1.113439999999999941e+01 1.280000000000000071e+01 +3.527000000000000000e+03 5.089229999999999876e+01 9.404999999999999361e+00 1.590000000000000036e+01 +3.540000000000000000e+03 5.084459999999999980e+01 7.371999999999999886e+00 1.789999999999999858e+01 +3.545000000000000000e+03 4.934400000000000119e+01 7.229700000000000237e+00 1.839999999999999858e+01 +3.571000000000000000e+03 4.981739999999999924e+01 1.186379999999999946e+01 1.369999999999999929e+01 +3.591000000000000000e+03 5.067430000000000234e+01 6.424000000000000377e+00 1.240000000000000036e+01 +3.603000000000000000e+03 4.938949999999999818e+01 9.966699999999999449e+00 1.459999999999999964e+01 +3.612000000000000000e+03 5.267110000000000269e+01 9.222899999999999210e+00 1.919999999999999929e+01 +3.621000000000000000e+03 4.882529999999999859e+01 1.050670000000000037e+01 1.390000000000000036e+01 +3.623000000000000000e+03 5.082939999999999969e+01 6.660199999999999676e+00 1.459999999999999964e+01 +3.631000000000000000e+03 5.371229999999999905e+01 7.151900000000000368e+00 1.380000000000000071e+01 +3.639000000000000000e+03 5.376469999999999771e+01 8.658300000000000551e+00 1.480000000000000071e+01 +3.660000000000000000e+03 5.036019999999999897e+01 6.869699999999999918e+00 1.469999999999999929e+01 +3.667000000000000000e+03 4.942580000000000240e+01 1.125380000000000003e+01 1.340000000000000036e+01 +3.668000000000000000e+03 4.950300000000000011e+01 1.105489999999999995e+01 1.419999999999999929e+01 +3.679000000000000000e+03 4.761869999999999692e+01 1.216649999999999920e+01 1.569999999999999929e+01 +3.730000000000000000e+03 4.739840000000000231e+01 1.027590000000000003e+01 1.450000000000000000e+01 +3.734000000000000000e+03 4.912800000000000011e+01 9.352499999999999147e+00 1.700000000000000000e+01 +3.739000000000000000e+03 4.945210000000000150e+01 1.243650000000000055e+01 1.180000000000000071e+01 +3.761000000000000000e+03 4.920700000000000074e+01 9.517599999999999838e+00 1.660000000000000142e+01 +3.811000000000000000e+03 5.129599999999999937e+01 1.309280000000000044e+01 1.950000000000000000e+01 +3.821000000000000000e+03 5.108729999999999905e+01 1.192919999999999980e+01 1.950000000000000000e+01 +3.836000000000000000e+03 5.045380000000000109e+01 1.022109999999999985e+01 1.700000000000000000e+01 +3.857000000000000000e+03 4.763620000000000232e+01 1.038920000000000066e+01 1.159999999999999964e+01 +3.875000000000000000e+03 4.915100000000000335e+01 1.168960000000000043e+01 1.230000000000000071e+01 +3.897000000000000000e+03 5.408930000000000149e+01 1.087729999999999997e+01 1.669999999999999929e+01 +3.904000000000000000e+03 4.953540000000000276e+01 6.378899999999999793e+00 1.880000000000000071e+01 +3.925000000000000000e+03 4.893289999999999651e+01 8.697300000000000253e+00 1.290000000000000036e+01 +3.927000000000000000e+03 4.793449999999999989e+01 9.286899999999999267e+00 1.130000000000000071e+01 +3.939000000000000000e+03 4.919120000000000203e+01 7.587900000000000311e+00 1.569999999999999929e+01 +3.946000000000000000e+03 5.048190000000000310e+01 1.213000000000000078e+01 1.719999999999999929e+01 +3.975000000000000000e+03 4.947769999999999868e+01 1.153570000000000029e+01 1.269999999999999929e+01 +3.987000000000000000e+03 5.238130000000000308e+01 1.306220000000000070e+01 1.989999999999999858e+01 +4.024000000000000000e+03 5.436430000000000007e+01 1.347710000000000008e+01 1.800000000000000000e+01 +4.032000000000000000e+03 5.179529999999999745e+01 1.113199999999999967e+01 1.950000000000000000e+01 +4.036000000000000000e+03 5.138949999999999818e+01 1.154119999999999990e+01 1.930000000000000071e+01 +4.039000000000000000e+03 5.373310000000000031e+01 9.877599999999999270e+00 1.719999999999999929e+01 +4.063000000000000000e+03 5.244610000000000127e+01 8.590600000000000236e+00 1.730000000000000071e+01 +4.094000000000000000e+03 4.780619999999999692e+01 9.620599999999999596e+00 1.390000000000000036e+01 +4.104000000000000000e+03 4.904249999999999687e+01 1.210190000000000055e+01 1.430000000000000071e+01 +4.127000000000000000e+03 5.099060000000000059e+01 7.695800000000000196e+00 1.639999999999999858e+01 +4.160000000000000000e+03 4.874249999999999972e+01 8.923999999999999488e+00 1.130000000000000071e+01 +4.169000000000000000e+03 4.867029999999999745e+01 7.993900000000000006e+00 1.440000000000000036e+01 +4.175000000000000000e+03 4.755899999999999750e+01 7.772100000000000009e+00 1.430000000000000071e+01 +4.177000000000000000e+03 4.897259999999999991e+01 8.330099999999999838e+00 1.480000000000000071e+01 +4.189000000000000000e+03 4.814789999999999992e+01 9.459600000000000009e+00 1.209999999999999964e+01 +4.261000000000000000e+03 4.787530000000000285e+01 1.212800000000000011e+01 1.469999999999999929e+01 +4.271000000000000000e+03 5.418030000000000257e+01 1.208079999999999998e+01 1.669999999999999929e+01 +4.275000000000000000e+03 5.312879999999999825e+01 9.339800000000000324e+00 1.719999999999999929e+01 +4.280000000000000000e+03 4.921620000000000061e+01 1.110350000000000037e+01 1.380000000000000071e+01 +4.287000000000000000e+03 4.938479999999999848e+01 1.017319999999999958e+01 1.500000000000000000e+01 +4.300000000000000000e+03 4.818139999999999645e+01 8.635600000000000165e+00 1.140000000000000036e+01 +4.301000000000000000e+03 4.985020000000000095e+01 7.871000000000000441e+00 2.089999999999999858e+01 +4.323000000000000000e+03 4.964679999999999893e+01 7.883700000000000152e+00 1.509999999999999964e+01 +4.336000000000000000e+03 4.921280000000000143e+01 7.107700000000000351e+00 1.700000000000000000e+01 +4.349000000000000000e+03 4.895689999999999742e+01 9.070999999999999730e+00 1.369999999999999929e+01 +4.354000000000000000e+03 4.878320000000000078e+01 1.331460000000000043e+01 1.400000000000000000e+01 +4.371000000000000000e+03 5.210419999999999874e+01 8.752100000000000435e+00 1.860000000000000142e+01 +4.377000000000000000e+03 5.035179999999999723e+01 1.000339999999999918e+01 1.550000000000000000e+01 +4.393000000000000000e+03 5.432789999999999964e+01 8.603099999999999525e+00 1.469999999999999929e+01 +4.411000000000000000e+03 4.991949999999999932e+01 8.967100000000000293e+00 1.789999999999999858e+01 +4.445000000000000000e+03 5.176579999999999870e+01 1.065329999999999977e+01 1.569999999999999929e+01 +4.464000000000000000e+03 5.056790000000000163e+01 1.180410000000000004e+01 1.580000000000000071e+01 +4.466000000000000000e+03 5.452750000000000341e+01 9.548700000000000188e+00 1.569999999999999929e+01 +4.480000000000000000e+03 5.034470000000000312e+01 9.553399999999999892e+00 1.810000000000000142e+01 +4.501000000000000000e+03 5.065460000000000207e+01 1.076929999999999943e+01 1.180000000000000071e+01 +4.508000000000000000e+03 5.029679999999999751e+01 6.419400000000000439e+00 1.300000000000000000e+01 +4.548000000000000000e+03 5.018469999999999942e+01 1.207910000000000039e+01 1.490000000000000036e+01 +4.559000000000000000e+03 4.916440000000000055e+01 1.261749999999999972e+01 1.359999999999999964e+01 +4.560000000000000000e+03 5.049249999999999972e+01 9.122600000000000264e+00 1.730000000000000071e+01 +4.592000000000000000e+03 4.932780000000000342e+01 1.208709999999999951e+01 1.330000000000000071e+01 +4.605000000000000000e+03 5.064410000000000167e+01 1.119359999999999999e+01 1.830000000000000071e+01 +4.625000000000000000e+03 5.364249999999999829e+01 1.138719999999999999e+01 1.860000000000000142e+01 +4.642000000000000000e+03 5.289110000000000156e+01 1.172969999999999935e+01 2.050000000000000000e+01 +4.651000000000000000e+03 5.190400000000000347e+01 1.018849999999999945e+01 1.839999999999999858e+01 +4.703000000000000000e+03 4.807189999999999941e+01 9.194300000000000139e+00 1.200000000000000000e+01 +4.706000000000000000e+03 4.827179999999999893e+01 1.302730000000000032e+01 1.400000000000000000e+01 +4.709000000000000000e+03 4.999960000000000093e+01 7.598099999999999632e+00 1.540000000000000036e+01 +4.745000000000000000e+03 5.296039999999999992e+01 9.792999999999999261e+00 1.819999999999999929e+01 +4.763000000000000000e+03 5.106069999999999709e+01 9.926600000000000534e+00 1.789999999999999858e+01 +4.841000000000000000e+03 5.369460000000000122e+01 8.873499999999999943e+00 1.509999999999999964e+01 +4.857000000000000000e+03 5.355340000000000344e+01 9.609700000000000131e+00 1.750000000000000000e+01 +4.878000000000000000e+03 5.166460000000000008e+01 1.088109999999999999e+01 1.680000000000000071e+01 +4.887000000000000000e+03 4.866559999999999775e+01 9.864800000000000679e+00 1.140000000000000036e+01 +4.896000000000000000e+03 5.466539999999999822e+01 9.804999999999999716e+00 1.519999999999999929e+01 +4.911000000000000000e+03 4.882750000000000057e+01 1.255969999999999942e+01 1.350000000000000000e+01 +4.928000000000000000e+03 4.882809999999999917e+01 9.199999999999999289e+00 1.269999999999999929e+01 +4.931000000000000000e+03 4.868829999999999814e+01 9.223499999999999588e+00 1.230000000000000071e+01 +4.978000000000000000e+03 5.063900000000000290e+01 1.002280000000000015e+01 1.610000000000000142e+01 +4.997000000000000000e+03 5.097710000000000008e+01 1.234190000000000076e+01 1.989999999999999858e+01 +5.009000000000000000e+03 5.376100000000000279e+01 1.255739999999999945e+01 1.810000000000000142e+01 +5.014000000000000000e+03 5.327579999999999671e+01 8.985699999999999577e+00 1.650000000000000000e+01 +5.017000000000000000e+03 5.040019999999999811e+01 1.138889999999999958e+01 1.440000000000000036e+01 +5.029000000000000000e+03 4.947370000000000090e+01 7.038499999999999979e+00 1.639999999999999858e+01 +5.046000000000000000e+03 4.985759999999999792e+01 1.235420000000000051e+01 1.380000000000000071e+01 +5.064000000000000000e+03 5.128970000000000340e+01 6.443699999999999761e+00 1.680000000000000071e+01 +5.097000000000000000e+03 5.406609999999999872e+01 1.276750000000000007e+01 1.919999999999999929e+01 +5.099000000000000000e+03 4.973259999999999792e+01 6.613100000000000200e+00 1.880000000000000071e+01 +5.100000000000000000e+03 4.974790000000000134e+01 6.658299999999999663e+00 1.860000000000000142e+01 +5.109000000000000000e+03 5.359969999999999857e+01 1.330390000000000050e+01 2.000000000000000000e+01 +5.111000000000000000e+03 4.803110000000000213e+01 1.253960000000000008e+01 1.359999999999999964e+01 +5.133000000000000000e+03 5.133440000000000225e+01 8.913199999999999790e+00 1.860000000000000142e+01 +5.142000000000000000e+03 5.374439999999999884e+01 1.406969999999999921e+01 1.950000000000000000e+01 +5.146000000000000000e+03 5.294140000000000157e+01 1.052890000000000015e+01 2.010000000000000142e+01 +5.149000000000000000e+03 4.957410000000000139e+01 1.019149999999999956e+01 1.630000000000000071e+01 +5.158000000000000000e+03 5.216009999999999991e+01 1.117590000000000039e+01 1.919999999999999929e+01 +5.229000000000000000e+03 4.804529999999999745e+01 8.460800000000000765e+00 1.059999999999999964e+01 +5.275000000000000000e+03 4.924450000000000216e+01 8.537399999999999878e+00 1.710000000000000142e+01 +5.279000000000000000e+03 5.161939999999999884e+01 9.574899999999999523e+00 1.500000000000000000e+01 +5.280000000000000000e+03 5.392240000000000322e+01 1.022669999999999924e+01 1.780000000000000071e+01 +5.300000000000000000e+03 5.025959999999999894e+01 8.360699999999999577e+00 1.660000000000000142e+01 +5.335000000000000000e+03 5.089629999999999654e+01 1.054840000000000089e+01 1.739999999999999858e+01 +5.347000000000000000e+03 5.150390000000000157e+01 9.111800000000000566e+00 1.789999999999999858e+01 +5.349000000000000000e+03 5.351959999999999695e+01 1.266539999999999999e+01 2.130000000000000071e+01 +5.371000000000000000e+03 5.049730000000000274e+01 9.942700000000000315e+00 1.159999999999999964e+01 +5.397000000000000000e+03 4.966629999999999967e+01 1.218449999999999989e+01 1.369999999999999929e+01 +5.404000000000000000e+03 4.840240000000000009e+01 1.169459999999999944e+01 1.350000000000000000e+01 +5.424000000000000000e+03 5.101769999999999783e+01 1.135440000000000005e+01 1.980000000000000071e+01 +5.426000000000000000e+03 4.937579999999999814e+01 8.121299999999999741e+00 1.300000000000000000e+01 +5.433000000000000000e+03 4.955340000000000344e+01 6.812000000000000277e+00 1.780000000000000071e+01 +5.440000000000000000e+03 4.901149999999999807e+01 1.093079999999999963e+01 1.400000000000000000e+01 +5.480000000000000000e+03 5.157630000000000337e+01 7.887900000000000134e+00 1.800000000000000000e+01 +5.490000000000000000e+03 5.184539999999999793e+01 1.076859999999999928e+01 1.810000000000000142e+01 +5.516000000000000000e+03 5.452830000000000155e+01 1.106060000000000088e+01 1.500000000000000000e+01 +5.538000000000000000e+03 4.788269999999999982e+01 1.115760000000000041e+01 1.269999999999999929e+01 +5.541000000000000000e+03 5.013199999999999790e+01 8.317000000000000171e+00 1.689999999999999858e+01 +5.546000000000000000e+03 5.212069999999999936e+01 1.245850000000000080e+01 1.910000000000000142e+01 +5.562000000000000000e+03 4.865160000000000196e+01 8.680099999999999483e+00 1.109999999999999964e+01 +5.629000000000000000e+03 5.188920000000000243e+01 1.264450000000000074e+01 2.060000000000000142e+01 +5.640000000000000000e+03 5.355040000000000333e+01 7.667200000000000237e+00 1.409999999999999964e+01 +5.643000000000000000e+03 5.318639999999999901e+01 1.249489999999999945e+01 2.200000000000000000e+01 +5.664000000000000000e+03 4.829529999999999745e+01 8.239100000000000534e+00 1.340000000000000036e+01 +5.676000000000000000e+03 5.239620000000000033e+01 1.068919999999999959e+01 1.969999999999999929e+01 +5.688000000000000000e+03 4.770029999999999859e+01 8.105700000000000571e+00 1.059999999999999964e+01 +5.692000000000000000e+03 4.960510000000000019e+01 8.365899999999999892e+00 1.689999999999999858e+01 +5.705000000000000000e+03 4.977040000000000219e+01 9.957599999999999341e+00 1.719999999999999929e+01 +5.715000000000000000e+03 5.246050000000000324e+01 9.431100000000000705e+00 2.000000000000000000e+01 +5.717000000000000000e+03 5.122560000000000002e+01 7.105199999999999960e+00 1.639999999999999858e+01 +5.731000000000000000e+03 4.767830000000000013e+01 8.380100000000000549e+00 1.390000000000000036e+01 +5.745000000000000000e+03 5.296640000000000015e+01 1.332680000000000042e+01 2.100000000000000000e+01 +5.750000000000000000e+03 5.103139999999999787e+01 1.214949999999999974e+01 2.019999999999999929e+01 +5.779000000000000000e+03 5.073140000000000072e+01 1.375159999999999982e+01 1.309999999999999964e+01 +5.792000000000000000e+03 4.742099999999999937e+01 1.098479999999999990e+01 2.799999999999999822e+00 +5.797000000000000000e+03 5.068789999999999907e+01 1.243290000000000006e+01 1.750000000000000000e+01 +5.800000000000000000e+03 4.902799999999999869e+01 1.323850000000000016e+01 1.250000000000000000e+01 +5.822000000000000000e+03 5.286299999999999955e+01 8.698800000000000310e+00 1.689999999999999858e+01 +5.825000000000000000e+03 5.261979999999999791e+01 1.278669999999999973e+01 2.089999999999999858e+01 +5.839000000000000000e+03 5.338810000000000144e+01 7.228699999999999903e+00 1.509999999999999964e+01 +5.856000000000000000e+03 4.854509999999999792e+01 1.335319999999999929e+01 1.319999999999999929e+01 +5.871000000000000000e+03 4.994619999999999749e+01 7.264499999999999957e+00 1.650000000000000000e+01 +5.906000000000000000e+03 4.950619999999999976e+01 8.558500000000000441e+00 1.630000000000000071e+01 +5.930000000000000000e+03 5.464099999999999824e+01 1.002379999999999960e+01 1.490000000000000036e+01 +5.941000000000000000e+03 4.767540000000000333e+01 1.246979999999999933e+01 1.440000000000000036e+01 +6.093000000000000000e+03 5.321390000000000242e+01 1.047039999999999971e+01 1.839999999999999858e+01 +6.105000000000000000e+03 5.431940000000000168e+01 9.805099999999999483e+00 1.569999999999999929e+01 +6.109000000000000000e+03 5.338369999999999749e+01 1.437279999999999980e+01 2.089999999999999858e+01 +6.129000000000000000e+03 5.105930000000000035e+01 1.442660000000000053e+01 1.939999999999999858e+01 +6.157000000000000000e+03 5.364099999999999824e+01 8.080799999999999983e+00 1.430000000000000071e+01 +6.158000000000000000e+03 4.922469999999999857e+01 1.060839999999999961e+01 1.369999999999999929e+01 +6.159000000000000000e+03 5.295420000000000016e+01 7.319600000000000328e+00 1.519999999999999929e+01 +6.163000000000000000e+03 5.416539999999999822e+01 1.035190000000000055e+01 1.619999999999999929e+01 +6.170000000000000000e+03 5.201919999999999789e+01 1.472540000000000049e+01 1.960000000000000142e+01 +6.197000000000000000e+03 5.186639999999999873e+01 9.271000000000000796e+00 1.719999999999999929e+01 +6.199000000000000000e+03 5.424839999999999662e+01 1.304180000000000028e+01 1.930000000000000071e+01 +6.217000000000000000e+03 4.924060000000000059e+01 6.935100000000000264e+00 1.860000000000000142e+01 +6.258000000000000000e+03 4.768449999999999989e+01 9.440899999999999181e+00 1.530000000000000071e+01 +6.259000000000000000e+03 4.902100000000000080e+01 9.603300000000000836e+00 1.430000000000000071e+01 +6.260000000000000000e+03 4.933279999999999887e+01 9.704000000000000625e+00 1.559999999999999964e+01 +6.262000000000000000e+03 4.876950000000000074e+01 9.873699999999999477e+00 1.430000000000000071e+01 +6.263000000000000000e+03 4.777380000000000138e+01 8.821899999999999409e+00 1.359999999999999964e+01 +6.264000000000000000e+03 5.141400000000000148e+01 8.650000000000000355e+00 1.580000000000000071e+01 +6.265000000000000000e+03 5.236129999999999995e+01 1.238669999999999938e+01 2.219999999999999929e+01 +6.266000000000000000e+03 5.203040000000000020e+01 1.096260000000000012e+01 1.860000000000000142e+01 +6.272000000000000000e+03 5.084259999999999735e+01 1.025179999999999936e+01 1.630000000000000071e+01 +6.273000000000000000e+03 5.250750000000000028e+01 1.185510000000000019e+01 2.050000000000000000e+01 +6.275000000000000000e+03 4.867049999999999699e+01 9.462699999999999889e+00 1.369999999999999929e+01 +6.305000000000000000e+03 5.120609999999999928e+01 1.049779999999999980e+01 1.930000000000000071e+01 +6.310000000000000000e+03 5.410490000000000066e+01 1.382390000000000008e+01 1.869999999999999929e+01 +6.312000000000000000e+03 4.953139999999999787e+01 1.064179999999999993e+01 1.480000000000000071e+01 +6.314000000000000000e+03 5.105069999999999908e+01 1.330030000000000001e+01 1.769999999999999929e+01 +6.336000000000000000e+03 5.001319999999999766e+01 9.653999999999999915e+00 1.590000000000000036e+01 +6.337000000000000000e+03 5.176630000000000109e+01 7.519400000000000084e+00 1.730000000000000071e+01 +6.344000000000000000e+03 5.039399999999999835e+01 8.142300000000000537e+00 1.950000000000000000e+01 +6.346000000000000000e+03 4.820700000000000074e+01 1.120350000000000001e+01 1.290000000000000036e+01 +6.347000000000000000e+03 5.005789999999999651e+01 1.029720000000000013e+01 1.710000000000000142e+01 +7.075000000000000000e+03 4.870199999999999818e+01 1.184929999999999950e+01 1.309999999999999964e+01 +7.099000000000000000e+03 5.201109999999999900e+01 1.039659999999999940e+01 1.660000000000000142e+01 +7.105000000000000000e+03 4.783500000000000085e+01 1.265479999999999983e+01 1.350000000000000000e+01 +7.106000000000000000e+03 5.207139999999999702e+01 8.456500000000000128e+00 1.789999999999999858e+01 +7.187000000000000000e+03 4.976359999999999673e+01 9.406299999999999883e+00 1.710000000000000142e+01 +7.298000000000000000e+03 5.452680000000000149e+01 9.042500000000000426e+00 1.509999999999999964e+01 +7.319000000000000000e+03 4.873740000000000094e+01 1.073930000000000007e+01 1.450000000000000000e+01 +7.321000000000000000e+03 5.115070000000000050e+01 1.133210000000000051e+01 1.939999999999999858e+01 +7.329000000000000000e+03 5.054670000000000130e+01 1.228630000000000067e+01 1.600000000000000000e+01 +7.330000000000000000e+03 5.146329999999999671e+01 7.977999999999999758e+00 1.700000000000000000e+01 +7.331000000000000000e+03 4.860990000000000322e+01 1.026740000000000030e+01 1.269999999999999929e+01 +7.341000000000000000e+03 5.009000000000000341e+01 8.786199999999999122e+00 1.750000000000000000e+01 +7.343000000000000000e+03 5.061990000000000123e+01 1.348160000000000025e+01 1.390000000000000036e+01 +7.350000000000000000e+03 4.910880000000000223e+01 1.282310000000000016e+01 1.190000000000000036e+01 +7.351000000000000000e+03 5.331750000000000256e+01 1.341750000000000043e+01 2.110000000000000142e+01 +7.364000000000000000e+03 5.168200000000000216e+01 1.230419999999999980e+01 2.010000000000000142e+01 +7.367000000000000000e+03 5.196430000000000149e+01 9.807199999999999918e+00 1.889999999999999858e+01 +7.368000000000000000e+03 5.100070000000000192e+01 1.036209999999999987e+01 1.600000000000000000e+01 +7.369000000000000000e+03 4.916230000000000189e+01 1.036609999999999943e+01 1.319999999999999929e+01 +7.370000000000000000e+03 4.939099999999999824e+01 1.268379999999999974e+01 1.219999999999999929e+01 +7.373000000000000000e+03 5.359839999999999804e+01 6.702399999999999913e+00 1.430000000000000071e+01 +7.374000000000000000e+03 5.208129999999999882e+01 6.940900000000000070e+00 1.469999999999999929e+01 +7.389000000000000000e+03 5.274609999999999843e+01 1.384270000000000067e+01 2.139999999999999858e+01 +7.393000000000000000e+03 5.144930000000000092e+01 1.425329999999999941e+01 1.989999999999999858e+01 +7.394000000000000000e+03 5.003150000000000119e+01 1.197450000000000081e+01 1.359999999999999964e+01 +7.395000000000000000e+03 4.865950000000000131e+01 1.253880000000000017e+01 1.380000000000000071e+01 +7.396000000000000000e+03 5.050840000000000174e+01 9.224700000000000344e+00 1.350000000000000000e+01 +7.403000000000000000e+03 4.779549999999999699e+01 1.003240000000000087e+01 1.230000000000000071e+01 +7.410000000000000000e+03 5.075130000000000052e+01 9.022399999999999309e+00 1.650000000000000000e+01 +7.412000000000000000e+03 5.000829999999999842e+01 9.423799999999999955e+00 1.509999999999999964e+01 +7.419000000000000000e+03 5.066100000000000136e+01 1.207559999999999967e+01 1.810000000000000142e+01 +7.420000000000000000e+03 5.110439999999999827e+01 1.171119999999999983e+01 1.960000000000000142e+01 +7.424000000000000000e+03 4.777239999999999753e+01 1.290729999999999933e+01 1.639999999999999858e+01 +7.427000000000000000e+03 5.401879999999999882e+01 9.925499999999999545e+00 1.660000000000000142e+01 +7.428000000000000000e+03 5.041669999999999874e+01 1.081559999999999988e+01 1.700000000000000000e+01 +7.431000000000000000e+03 4.801299999999999812e+01 1.155240000000000045e+01 1.290000000000000036e+01 +7.432000000000000000e+03 5.264229999999999876e+01 1.066269999999999918e+01 1.919999999999999929e+01 +1.367000000000000000e+04 5.150880000000000081e+01 6.701800000000000423e+00 1.730000000000000071e+01 +1.367400000000000000e+04 4.929429999999999978e+01 8.905300000000000438e+00 1.550000000000000000e+01 +1.367500000000000000e+04 5.208180000000000121e+01 9.407700000000000173e+00 2.039999999999999858e+01 +1.369600000000000000e+04 5.159660000000000224e+01 7.404799999999999827e+00 1.710000000000000142e+01 +1.370000000000000000e+04 5.133290000000000219e+01 7.341099999999999959e+00 1.580000000000000071e+01 +1.371000000000000000e+04 4.857339999999999947e+01 1.225760000000000005e+01 1.269999999999999929e+01 +1.371100000000000000e+04 5.068200000000000216e+01 1.151500000000000057e+01 1.839999999999999858e+01 +1.371300000000000000e+04 5.108990000000000009e+01 7.628899999999999793e+00 1.650000000000000000e+01 +1.377700000000000000e+04 5.224669999999999703e+01 1.095919999999999916e+01 1.980000000000000071e+01 +1.396500000000000000e+04 4.826389999999999958e+01 8.813399999999999679e+00 1.040000000000000036e+01 +1.500000000000000000e+04 5.079829999999999757e+01 6.024399999999999977e+00 1.290000000000000036e+01 +1.520700000000000000e+04 5.128349999999999653e+01 9.358999999999999986e+00 1.710000000000000142e+01 +1.544400000000000000e+04 4.844180000000000064e+01 9.921599999999999753e+00 1.169999999999999929e+01 +1.555500000000000000e+04 4.787610000000000099e+01 1.058489999999999931e+01 1.080000000000000071e+01 diff --git a/GSTools/source/examples/09_spatio_temporal/01_precip_1d.py b/GSTools/source/examples/09_spatio_temporal/01_precip_1d.py new file mode 100644 index 0000000000000000000000000000000000000000..4b4c6b8a15931735c2be407edebb94fe94d7a833 --- /dev/null +++ b/GSTools/source/examples/09_spatio_temporal/01_precip_1d.py @@ -0,0 +1,130 @@ +""" +Creating a 1D Synthetic Precipitation Field +------------------------------------------- + +In this example we will create a time series of a 1D synthetic precipitation +field. + +We'll start off by creating a Gaussian random field with an exponential +variogram, which seems to reproduce the spatial correlations of precipitation +fields quite well. We'll create a daily timeseries over a one dimensional cross +section of 50km. This workflow is suited for sub daily precipitation time +series. +""" + +import copy + +import matplotlib.pyplot as plt +import numpy as np + +import gstools as gs + +# fix the seed for reproducibility +seed = 20170521 +# spatial axis of 50km with a resolution of 1km +x = np.arange(0, 50, 1.0) +# half daily timesteps over three months +t = np.arange(0.0, 90.0, 0.5) + +# space-time anisotropy ratio given in units d / km +st_anis = 0.4 + +# an exponential variogram with a corr. lengths of 2d and 5km +model = gs.Exponential( + temporal=True, spatial_dim=1, var=1, len_scale=5, anis=st_anis +) +# create a spatial random field instance +srf = gs.SRF(model, seed=seed) + +pos, time = [x], [t] + +# a Gaussian random field which is also saved internally for the transformations +srf.structured(pos + time) +P_gau = copy.deepcopy(srf.field) + +############################################################################### +# Next, we could take care of the dry periods. Therefore we would simply +# introduce a lower threshold value. But we will combine this step with the +# next one. Anyway, for demonstration purposes, we will also do it with the +# threshold value now. + +threshold = 0.85 +P_cut = copy.deepcopy(srf.field) +P_cut[P_cut <= threshold] = 0.0 + +############################################################################### +# With the above lines of code we have created a cut off Gaussian spatial +# random field with an exponential variogram. But precipitation fields are not +# distributed Gaussian. Thus, we will now transform the field with an inverse +# box-cox transformation (create a non-Gaussian field) , which is often used to +# account for the skewness of precipitation fields. Different values have been +# suggested for the transformation parameter lambda, but we will stick to 1/2. +# As already mentioned, we will perform the cutoff for the dry periods with +# this transformation implicitly with the shift. The warning will tell you +# that values have indeed been cut off and it can be ignored. We call the +# resulting field Gaussian anamorphosis. + +# the lower this value, the more will be cut off, a value of 0.2 cuts off +# nearly everything in this example. +cutoff = 0.55 +gs.transform.boxcox(srf, lmbda=0.5, shift=-1.0 / cutoff) + +############################################################################### +# As a last step, the amount of precipitation is set. This should of course be +# calibrated towards observations (the same goes for the threshold, the +# variance, correlation length, and so on). + +amount = 2.0 +srf.field *= amount +P_ana = srf.field + +############################################################################### +# Finally we can have a look at the fields resulting from each step. Note, that +# the cutoff of the cut Gaussian only approximates the cutoff values from the +# box-cox transformation. For a closer look, we will examine a cross section +# at an arbitrary location. And afterwards we will create a contour plot for +# visual candy. + +fig, axs = plt.subplots(2, 2, sharex=True, sharey=True) + +axs[0, 0].set_title("Gaussian") +axs[0, 0].plot(t, P_gau[20, :]) +axs[0, 0].set_ylabel(r"$P$ / mm") + +axs[0, 1].set_title("Cut Gaussian") +axs[0, 1].plot(t, P_cut[20, :]) + +axs[1, 0].set_title("Cut Gaussian Anamorphosis") +axs[1, 0].plot(t, P_ana[20, :]) +axs[1, 0].set_xlabel(r"$t$ / d") +axs[1, 0].set_ylabel(r"$P$ / mm") + +axs[1, 1].set_title("Different Cross Section") +axs[1, 1].plot(t, P_ana[10, :]) +axs[1, 1].set_xlabel(r"$t$ / d") + +plt.tight_layout() + +fig, axs = plt.subplots(2, 2, sharex=True, sharey=True) + +axs[0, 0].set_title("Gaussian") +cont = axs[0, 0].contourf(t, x, P_gau, cmap="PuBu", levels=10) +cbar = fig.colorbar(cont, ax=axs[0, 0]) +cbar.ax.set_ylabel(r"$P$ / mm") +axs[0, 0].set_ylabel(r"$x$ / km") + +axs[0, 1].set_title("Cut Gaussian") +cont = axs[0, 1].contourf(t, x, P_cut, cmap="PuBu", levels=10) +cbar = fig.colorbar(cont, ax=axs[0, 1]) +cbar.ax.set_ylabel(r"$P$ / mm") +axs[0, 1].set_xlabel(r"$t$ / d") + +axs[1, 0].set_title("Cut Gaussian Anamorphosis") +cont = axs[1, 0].contourf(t, x, P_ana, cmap="PuBu", levels=10) +cbar = fig.colorbar(cont, ax=axs[1, 0]) +cbar.ax.set_ylabel(r"$P$ / mm") +axs[1, 0].set_xlabel(r"$t$ / d") +axs[1, 0].set_ylabel(r"$x$ / km") + +fig.delaxes(axs[1, 1]) +plt.tight_layout() diff --git a/GSTools/source/examples/09_spatio_temporal/02_precip_2d.py b/GSTools/source/examples/09_spatio_temporal/02_precip_2d.py new file mode 100644 index 0000000000000000000000000000000000000000..81c7896493103585d8db41f9af339bf314e7a672 --- /dev/null +++ b/GSTools/source/examples/09_spatio_temporal/02_precip_2d.py @@ -0,0 +1,76 @@ +""" +Creating a 2D Synthetic Precipitation Field +------------------------------------------- + +In this example we'll create a time series of a 2D synthetic precipitation +field. + +Very similar to the previous tutorial, we'll start off by creating a Gaussian +random field with an exponential variogram, which seems to reproduce the +spatial correlations of precipitation fields quite well. We'll create a daily +timeseries over a two dimensional domain of 50km x 40km. This workflow is +suited for sub daily precipitation time series. +""" + +import matplotlib.animation as animation +import matplotlib.pyplot as plt +import numpy as np + +import gstools as gs + +# fix the seed for reproducibility +seed = 20170521 +# 1st spatial axis of 50km with a resolution of 1km +x = np.arange(0, 50, 1.0) +# 2nd spatial axis of 40km with a resolution of 1km +y = np.arange(0, 40, 1.0) +# half daily timesteps over three months +t = np.arange(0.0, 90.0, 0.5) + +# space-time anisotropy ratio given in units d / km +st_anis = 0.4 + +# an exponential variogram with a corr. lengths of 5km, 5km, and 2d +model = gs.Exponential( + temporal=True, spatial_dim=2, var=1, len_scale=5, anis=st_anis +) +# create a spatial random field instance +srf = gs.SRF(model, seed=seed) + +pos, time = [x, y], [t] + +# the Gaussian random field +srf.structured(pos + time) + +# account for the skewness and the dry periods +cutoff = 0.55 +gs.transform.boxcox(srf, lmbda=0.5, shift=-1.0 / cutoff) + +# adjust the amount of precipitation +amount = 4.0 +srf.field *= amount + +############################################################################### +# plot the 2d precipitation field over time as an animation. + + +def _update_ani(time_step): + im.set_array(srf.field[:, :, time_step].T) + return (im,) + + +fig, ax = plt.subplots() +im = ax.imshow( + srf.field[:, :, 0].T, + cmap="Blues", + interpolation="bicubic", + origin="lower", +) +cbar = fig.colorbar(im) +cbar.ax.set_ylabel(r"Precipitation $P$ / mm") +ax.set_xlabel(r"$x$ / km") +ax.set_ylabel(r"$y$ / km") + +ani = animation.FuncAnimation( + fig, _update_ani, len(t), interval=100, blit=True +) diff --git a/GSTools/source/examples/09_spatio_temporal/03_geographic_coordinates.py b/GSTools/source/examples/09_spatio_temporal/03_geographic_coordinates.py new file mode 100644 index 0000000000000000000000000000000000000000..b1cfbff6475a3f7afeb10815b1f889e0eb6565d9 --- /dev/null +++ b/GSTools/source/examples/09_spatio_temporal/03_geographic_coordinates.py @@ -0,0 +1,38 @@ +""" +Working with spatio-temporal lat-lon fields +------------------------------------------- + +In this example, we demonstrate how to generate a spatio-temporal +random field on geographical coordinates. + +First we setup a model, with ``latlon=True`` and ``temporal=True``, +to get the associated spatio-temporal Yadrenko model. + +In addition, we will use a kilometer scale provided by :any:`KM_SCALE` +as ``geo_scale`` to have a meaningful length scale in km. +By default the length scale would be given in radians (:any:`RADIAN_SCALE`). +A third option is a length scale in degrees (:any:`DEGREE_SCALE`). + +To generate the field, we simply pass ``(lat, lon, time)`` as the position tuple +to the :any:`SRF` class. + +We will set a spatial length-scale of `1000` and a time length-scale of `100` days. +""" + +import numpy as np + +import gstools as gs + +model = gs.Matern( + latlon=True, + temporal=True, + var=1, + len_scale=[1000, 100], + geo_scale=gs.KM_SCALE, +) + +lat = lon = np.linspace(-80, 81, 50) +time = np.linspace(0, 777, 50) +srf = gs.SRF(model, seed=1234) +field = srf.structured((lat, lon, time)) +srf.plot() diff --git a/GSTools/source/examples/09_spatio_temporal/README.rst b/GSTools/source/examples/09_spatio_temporal/README.rst new file mode 100644 index 0000000000000000000000000000000000000000..3cb06b9e24e5816b470f7086b9d272ef947c0da6 --- /dev/null +++ b/GSTools/source/examples/09_spatio_temporal/README.rst @@ -0,0 +1,65 @@ +Spatio-Temporal Modeling +======================== + +Spatio-Temporal modelling can provide insights into time dependent processes +like rainfall, air temperature or crop yield. + +GSTools provides the metric spatio-temporal model for all covariance models +by setting ``temporal=True``, which enhances the spatial model dimension with +a time dimension to result in the spatio-temporal dimension. +Since the model dimension is then higher than the spatial dimension, you can use +the ``spatial_dim`` argument to explicitly set the spatial dimension. +Doing that and setting a spatio-temporal anisotropy ratio looks like this: + +.. code-block:: python + + import gstools as gs + dim = 3 # spatial dimension + st_anis = 0.4 + st_model = gs.Exponential(temporal=True, spatial_dim=dim, anis=st_anis) + +Since it is given in the name "spatio-temporal", time is always treated as last dimension. +You could also use ``dim`` to specify the dimension but note that it needs to include +the temporal dimension. + +There are now three different dimension attributes giving information about (i) the +model dimension (``dim``), (ii) the field dimension (``field_dim``, including time) and +(iii) the spatial dimension (``spatial_dim`` always 1 less than ``field_dim`` for temporal models). +Model and field dimension can differ in case of geographic coordinates where the model dimension is 3, +but the field or parametric dimension is 2. +If the model is spatio-temporal with geographic coordinates, the model dimension is 4, +the field dimension is 3 and the spatial dimension is 2. + +In the case above we get: + +.. code-block:: python + + st_model.dim == 4 + st_model.field_dim == 4 + st_model.spatial_dim == 3 + +This formulation enables us to have spatial anisotropy and rotation defined as in +non-temporal models, without altering the behavior in the time dimension: + +.. code-block:: python + + anis = [0.4, 0.2] # spatial anisotropy in 3D + angles = [0.5, 0.4, 0.3] # spatial rotation in 3D + st_model = gs.Exponential(temporal=True, spatial_dim=dim, anis=anis+[st_anis], angles=angles) + +In order to generate spatio-temporal position tuples, GSTools provides a +convenient function :any:`generate_st_grid`. The output can be used for +spatio-temporal random field generation (or kriging resp. conditioned fields): + +.. code-block:: python + + pos = dim * [1, 2, 3] # 3 points in space (1,1,1), (2,2,2) and (3,3,3) + time = range(10) # 10 time steps + st_grid = gs.generate_st_grid(pos, time) + st_rf = gs.SRF(st_model) + st_field = st_rf(st_grid).reshape(-1, len(time)) + +Then we can access the different time-steps by the last array index. + +Examples +-------- diff --git a/GSTools/source/examples/10_normalizer/00_lognormal_kriging.py b/GSTools/source/examples/10_normalizer/00_lognormal_kriging.py new file mode 100644 index 0000000000000000000000000000000000000000..9880bc37aba820775e3f6fff9e40025b12a1003c --- /dev/null +++ b/GSTools/source/examples/10_normalizer/00_lognormal_kriging.py @@ -0,0 +1,55 @@ +r""" +Log-Normal Kriging +------------------ + +Log Normal kriging is a term to describe a special workflow for kriging to +deal with log-normal data, like conductivity or transmissivity in hydrogeology. + +It simply means to first convert the input data to a normal distribution, i.e. +applying a logarithic function, then interpolating these values with kriging +and transforming the result back with the exponential function. + +The resulting kriging variance describes the error variance of the log-values +of the target variable. + +In this example we will use ordinary kriging. +""" + +import numpy as np + +import gstools as gs + +# condtions +cond_pos = [0.3, 1.9, 1.1, 3.3, 4.7] +cond_val = [0.47, 0.56, 0.74, 1.47, 1.74] +# resulting grid +gridx = np.linspace(0.0, 15.0, 151) +# stable covariance model +model = gs.Stable(dim=1, var=0.5, len_scale=2.56, alpha=1.9) + +############################################################################### +# In order to result in log-normal kriging, we will use the :any:`LogNormal` +# Normalizer. This is a parameter-less normalizer, so we don't have to fit it. +normalizer = gs.normalizer.LogNormal + +############################################################################### +# Now we generate the interpolated field as well as the mean field. +# This can be done by setting `only_mean=True` in :any:`Krige.__call__`. +# The result is then stored as `mean_field`. +# +# In terms of log-normal kriging, this mean represents the geometric mean of +# the field. +krige = gs.krige.Ordinary(model, cond_pos, cond_val, normalizer=normalizer) +# interpolate the field +krige(gridx) +# also generate the mean field +krige(gridx, only_mean=True) + +############################################################################### +# And that's it. Let's have a look at the results. +ax = krige.plot() +# plotting the geometric mean +krige.plot("mean_field", ax=ax) +# plotting the conditioning data +ax.scatter(cond_pos, cond_val, color="k", zorder=10, label="Conditions") +ax.legend() diff --git a/GSTools/source/examples/10_normalizer/01_auto_fit.py b/GSTools/source/examples/10_normalizer/01_auto_fit.py new file mode 100644 index 0000000000000000000000000000000000000000..71ad1385119cb9c7b5e2335a726cc9bd17f4088a --- /dev/null +++ b/GSTools/source/examples/10_normalizer/01_auto_fit.py @@ -0,0 +1,107 @@ +""" +Automatic fitting +----------------- + +In order to demonstrate how to automatically fit normalizer and variograms, +we generate synthetic log-normal data, that should be interpolated with +ordinary kriging. + +Normalizers are fitted by minimizing the likelihood function and variograms +are fitted by estimating the empirical variogram with automatic binning and +fitting the theoretical model to it. Thereby the sill is constrained to match +the field variance. + +Artificial data +^^^^^^^^^^^^^^^ + +Here we generate log-normal data following a Gaussian covariance model. +We will generate the "original" field on a 60x60 mesh, from which we will take +samples in order to pretend a situation of data-scarcity. +""" + +import matplotlib.pyplot as plt +import numpy as np + +import gstools as gs + +# structured field with edge length of 50 +x = y = range(51) +pos = gs.generate_grid([x, y]) +model = gs.Gaussian(dim=2, var=1, len_scale=10) +srf = gs.SRF(model, seed=20170519, normalizer=gs.normalizer.LogNormal()) +# generate the original field +srf(pos) + +############################################################################### +# Here, we sample 60 points and set the conditioning points and values. + +ids = np.arange(srf.field.size) +samples = np.random.RandomState(20210201).choice(ids, size=60, replace=False) + +# sample conditioning points from generated field +cond_pos = pos[:, samples] +cond_val = srf.field[samples] + +############################################################################### +# Fitting and Interpolation +# ^^^^^^^^^^^^^^^^^^^^^^^^^ +# +# Now we want to interpolate the "measured" samples +# and we want to normalize the given data with the BoxCox transformation. +# +# Here we set up the kriging routine and use a :any:`Stable` model, that should +# be fitted automatically to the given data +# and we pass the :any:`BoxCox` normalizer in order to gain normality. +# +# The normalizer will be fitted automatically to the data, +# by setting ``fit_normalizer=True``. +# +# The covariance/variogram model will be fitted by an automatic workflow +# by setting ``fit_variogram=True``. + +krige = gs.krige.Ordinary( + model=gs.Stable(dim=2), + cond_pos=cond_pos, + cond_val=cond_val, + normalizer=gs.normalizer.BoxCox(), + fit_normalizer=True, + fit_variogram=True, +) + +############################################################################### +# First, let's have a look at the fitting results: + +print(krige.model) +print(krige.normalizer) + +############################################################################### +# As we see, it went quite well. Variance is a bit underestimated, but +# length scale and nugget are good. The shape parameter of the stable model +# is correctly estimated to be close to `2`, +# so we result in a Gaussian like model. +# +# The BoxCox parameter `lmbda` was estimated to be almost 0, which means, +# the log-normal distribution was correctly fitted. +# +# Now let's run the kriging interpolation. + +krige(pos) + +############################################################################### +# Plotting +# ^^^^^^^^ +# +# Finally let's compare the original, sampled and interpolated fields. +# As we'll see, there is a lot of information in the covariance structure +# of the measurement samples and the field is reconstructed quite accurately. + +fig, ax = plt.subplots(1, 3, figsize=[8, 3]) +ax[0].imshow(srf.field.reshape(len(x), len(y)).T, origin="lower") +ax[1].scatter(*cond_pos, c=cond_val) +ax[2].imshow(krige.field.reshape(len(x), len(y)).T, origin="lower") +# titles +ax[0].set_title("original field") +ax[1].set_title("sampled field") +ax[2].set_title("interpolated field") +# set aspect ratio to equal in all plots +[ax[i].set_aspect("equal") for i in range(3)] diff --git a/GSTools/source/examples/10_normalizer/02_compare.py b/GSTools/source/examples/10_normalizer/02_compare.py new file mode 100644 index 0000000000000000000000000000000000000000..2dd74488c204b7979a3882345333f8fe6d36c866 --- /dev/null +++ b/GSTools/source/examples/10_normalizer/02_compare.py @@ -0,0 +1,67 @@ +""" +Normalizer Comparison +--------------------- + +Let's compare the transformation behavior of the provided normalizers. + +But first, we define a convenience routine and make some imports as always. +""" + +import matplotlib.pyplot as plt +import numpy as np + +import gstools as gs + + +def dashes(i=1, max_n=12, width=1): + """Return line dashes.""" + return i * [width, width] + [max_n * 2 * width - 2 * i * width, width] + + +############################################################################### +# We select 4 normalizers depending on a single parameter lambda and +# plot their transformation behavior within the interval [-5, 5]. +# +# For the shape parameter lambda, we create a list of 8 values ranging from +# -1 to 2.5. + +lmbdas = [i * 0.5 for i in range(-2, 6)] +normalizers = [ + gs.normalizer.BoxCox, + gs.normalizer.YeoJohnson, + gs.normalizer.Modulus, + gs.normalizer.Manly, +] + +############################################################################### +# Let's plot them! + +fig, ax = plt.subplots(2, 2, figsize=[8, 8]) +for i, norm in enumerate(normalizers): + # correctly setting the data range + x_rng = norm().normalize_range + x = np.linspace(max(-5, x_rng[0] + 0.01), min(5, x_rng[1] - 0.01)) + for j, lmbda in enumerate(lmbdas): + ax.flat[i].plot( + x, + norm(lmbda=lmbda).normalize(x), + label=r"$\lambda=" + str(lmbda) + "$", + color="k", + alpha=0.2 + j * 0.1, + dashes=dashes(j), + ) + # axis formatting + ax.flat[i].grid(which="both", color="grey", linestyle="-", alpha=0.2) + ax.flat[i].set_ylim((-5, 5)) + ax.flat[i].set_xlim((-5, 5)) + ax.flat[i].set_title(norm().name) +# figure formatting +handles, labels = ax.flat[-1].get_legend_handles_labels() +fig.legend(handles, labels, loc="lower center", ncol=4, handlelength=3.0) +fig.suptitle("Normalizer Comparison", fontsize=20) +fig.show() + +############################################################################### +# The missing :any:`LogNormal` transformation is covered by the :any:`BoxCox` +# transformation for lambda=0. The :any:`BoxCoxShift` transformation is +# simply the :any:`BoxCox` transformation shifted on the X-axis. diff --git a/GSTools/source/examples/10_normalizer/README.rst b/GSTools/source/examples/10_normalizer/README.rst new file mode 100644 index 0000000000000000000000000000000000000000..930756be9772aa348005cef6cf0d14cb3c018bb7 --- /dev/null +++ b/GSTools/source/examples/10_normalizer/README.rst @@ -0,0 +1,55 @@ +Normalizing Data +================ + +When dealing with real-world data, one can't assume it to be normal distributed. +In fact, many properties are modeled by applying different transformations, +for example conductivity is often assumed to be log-normal or precipitation +is transformed using the famous box-cox power transformation. + +These "normalizers" are often represented as parameteric power transforms and +one is interested in finding the best parameter to gain normality in the input +data. + +This is of special interest when kriging should be applied, since the target +variable of the kriging interpolation is assumed to be normal distributed. + +GSTools provides a set of Normalizers and routines to automatically fit these +to input data by minimizing the likelihood function. + +Mean, Trend and Normalizers +--------------------------- + +All Field classes (:any:`SRF`, :any:`Krige` or :any:`CondSRF`) provide the input +of `mean`, `normalizer` and `trend`: + +* A `trend` can be a callable function, that represents a trend in input data. + For example a linear decrease of temperature with height. + +* The `normalizer` will be applied after the data was detrended, i.e. the trend + was substracted from the data, in order to gain normality. + +* The `mean` is now interpreted as the mean of the normalized data. The user + could also provide a callable mean, but it is mostly meant to be constant. + +When no normalizer is given, `trend` and `mean` basically behave the same. +We just decided that a trend is associated with raw data and a mean is used +in the context of normally distributed data. + +Provided Normalizers +-------------------- + +The following normalizers can be passed to all Field-classes and variogram +estimation routines or can be used as standalone tools to analyse data. + +.. currentmodule:: gstools.normalizer + +.. autosummary:: + LogNormal + BoxCox + BoxCoxShift + YeoJohnson + Modulus + Manly + +Examples +-------- diff --git a/GSTools/source/examples/11_plurigaussian/00_simple.py b/GSTools/source/examples/11_plurigaussian/00_simple.py new file mode 100644 index 0000000000000000000000000000000000000000..ffc384c885d95cf83e4dfcb85ac9d291b9e7874e --- /dev/null +++ b/GSTools/source/examples/11_plurigaussian/00_simple.py @@ -0,0 +1,75 @@ +""" +A First and Simple Example +-------------------------- + +As a first example, we will create a two dimensional plurigaussian field +(PGS). Thus, we need two spatial random fields(SRF) and on top of that, we +need a field describing the categorical data and its spatial relation. +We will start off by creating the two SRFs with a Gaussian variogram, which +makes the fields nice and smooth. But before that, we will import all +necessary libraries and define a few variables, like the number of grid +cells in each dimension. +""" + +import matplotlib.pyplot as plt +import numpy as np + +import gstools as gs + +dim = 2 +# no. of cells in both dimensions +N = [180, 140] + +x = np.arange(N[0]) +y = np.arange(N[1]) + +############################################################################### +# In this first example we will use the same geostatistical parameters for +# both fields for simplicity. Thus, we can use the same SRF instance for the +# two fields. + +model = gs.Gaussian(dim=dim, var=1, len_scale=10) +srf = gs.SRF(model) +field1 = srf.structured([x, y], seed=20170519) +field2 = srf.structured([x, y], seed=19970221) + +############################################################################### +# Now, we will create the lithotypes field describing the categorical data. For +# now, we will only have two categories and we will address them by the +# integers 0 and 1. We start off by creating a matrix of 0s from which we will +# select a rectangle and fill that with 1s. This field does not have to match +# the shape of the SRFs. + +centroid = [200, 160] + +# size of the rectangle +rect = [40, 32] + +lithotypes = np.zeros(centroid) +lithotypes[ + centroid[0] // 2 - rect[0] // 2 : centroid[0] // 2 + rect[0] // 2, + centroid[1] // 2 - rect[1] // 2 : centroid[1] // 2 + rect[1] // 2, +] = 1 + +############################################################################### +# With the two SRFs and the L-field ready, we can create our first PGS. First, we +# set up an instance of the PGS class and then we are ready to calculate the +# field by calling the instance and handing over the L-field. + +pgs = gs.PGS(dim, [field1, field2]) +P = pgs(lithotypes) + +############################################################################### +# Finally, we can plot the PGS, but we will also show the L-field and the two +# original Gaussian fields. + +fig, axs = plt.subplots(2, 2) + +axs[0, 0].imshow(field1, cmap="copper", origin="lower") +axs[0, 1].imshow(field2, cmap="copper", origin="lower") +axs[1, 0].imshow(lithotypes, cmap="copper", origin="lower") +axs[1, 1].imshow(P, cmap="copper", origin="lower") + +# For more information on Plurigaussian fields and how they naturally extend +# truncated Gaussian fields, we can recommend the book +# [Plurigaussian Simulations in Geosciences](https://doi.org/10.1007/978-3-642-19607-2) diff --git a/GSTools/source/examples/11_plurigaussian/01_pgs.py b/GSTools/source/examples/11_plurigaussian/01_pgs.py new file mode 100644 index 0000000000000000000000000000000000000000..55ea060fb2046d4947888e7e52b45885e5f8cd75 --- /dev/null +++ b/GSTools/source/examples/11_plurigaussian/01_pgs.py @@ -0,0 +1,93 @@ +""" +Understanding PGS +----------------- + +In this example we want to try to understand how exactly PGS are generated +and how to influence them with the categorical field. +First of all, we will set everything up very similar to the first example. +""" + +import matplotlib.pyplot as plt +import numpy as np + +import gstools as gs + +dim = 2 +# no. of cells in both dimensions +N = [100, 80] + +x = np.arange(N[0]) +y = np.arange(N[1]) + +############################################################################### +# In this example we will use different geostatistical parameters for the +# SRFs. We will create fields with a strong anisotropy, and on top of that they +# will both be rotated. + +model1 = gs.Gaussian(dim=dim, var=1, len_scale=[20, 1], angles=np.pi / 8) +srf1 = gs.SRF(model1, seed=20170519) +field1 = srf1.structured([x, y]) +model2 = gs.Gaussian(dim=dim, var=1, len_scale=[1, 20], angles=np.pi / 4) +srf2 = gs.SRF(model2, seed=19970221) +field2 = srf2.structured([x, y]) +field1 += 5.0 + +############################################################################### +# Internally, each field's values are mapped along an axis, which can be nicely +# visualized with a scatter plot. We can easily do that by flattening the 2d +# field values and simply use matplotlib's scatter plotting functionality. +# The x-axis shows field1's values and the y-axis shows field2's values. + +plt.scatter(field1.flatten(), field2.flatten(), s=0.1) + +############################################################################### +# This mapping always has a multivariate Gaussian distribution and this is also +# the field on which we define our categorical data `lithotypes` and their +# relations to each other. Before providing further explanations, we will +# create the lithotypes field, which again will have only two categories, but +# this time we will not prescribe a rectangle, but a circle. + +# no. of grid cells of L-field +M = [51, 41] +# we need the indices of `lithotypes` later +x_lith = np.arange(M[0]) +y_lith = np.arange(M[1]) + +# radius of circle +radius = 7 + +lithotypes = np.zeros(M) +mask = (x_lith[:, np.newaxis] - M[0] // 2) ** 2 + ( + y_lith[np.newaxis, :] - M[1] // 2 +) ** 2 < radius**2 +lithotypes[mask] = 1 + +############################################################################### +# We can compute the actual PGS now. As a second step, we use a helper function +# to recalculate the axes on which the lithotypes are defined. Normally, this +# is handled internally. But in order to show the scatter plot together with +# the lithotypes, we need the axes here. + +pgs = gs.PGS(dim, [field1, field2]) +P = pgs(lithotypes) + +x_lith, y_lith = pgs.calc_lithotype_axes(lithotypes.shape) + +############################################################################### +# And now to some plotting. Unfortunately, matplotlib likes to mess around with +# the aspect ratios of the plots, so the left panel is a bit stretched. + +fig, axs = plt.subplots(2, 2) +axs[0, 0].imshow(field1, cmap="copper", origin="lower") +axs[0, 1].imshow(field2, cmap="copper", origin="lower") +axs[1, 0].scatter(field1.flatten(), field2.flatten(), s=0.1, color="C0") +axs[1, 0].pcolormesh(x_lith, y_lith, lithotypes.T, alpha=0.3, cmap="copper") + +axs[1, 1].imshow(P, cmap="copper", origin="lower") + +############################################################################### +# The black areas show the category 0 and the orange areas show category 1. We +# see that the majority of all points in the scatter plot are within the +# yellowish circle, thus the orange areas are larger than the black ones. The +# strong anisotropy and the rotation of the fields create these interesting +# patterns which remind us of fractures in the subsurface. diff --git a/GSTools/source/examples/11_plurigaussian/02_spatial_relations.py b/GSTools/source/examples/11_plurigaussian/02_spatial_relations.py new file mode 100644 index 0000000000000000000000000000000000000000..0380c89c33ebda824241d627b4c5edd0f0fbf005 --- /dev/null +++ b/GSTools/source/examples/11_plurigaussian/02_spatial_relations.py @@ -0,0 +1,106 @@ +""" +Controlling Spatial Relations +----------------------------- + +In this example we will try to understand how we can influence the spatial +relationships of the different categories with the lithotypes field. For +simplicity, we will start very similarly to the very first example. +""" + +import matplotlib.pyplot as plt +import numpy as np + +import gstools as gs + +dim = 2 +# no. of cells in both dimensions +N = [100, 80] + +x = np.arange(N[0]) +y = np.arange(N[1]) + +############################################################################### +# Again, we will use the same parameters for both fields. + +model = gs.Gaussian(dim=dim, var=1, len_scale=10) +srf = gs.SRF(model) +field1 = srf.structured([x, y], seed=20170519) +field2 = srf.structured([x, y], seed=19970221) + +############################################################################### +# Now, we will prepare the lithotypes field, which will be a bit more +# complicated this # time. First, we will create a triangle. Next, we will +# create two rectangles touching each other along one of their edges and both +# being directly above the triangle, but without touching it directly. +# Finally, we will create a few very narrow rectangles, which will not touch +# any other category shapes. The implementation details are not very +# interesting, and can be skipped. + +# no. of grid cells of lithotypes field +M = [60, 50] + +# size of the rectangles +rect = [10, 8] + +# positions of some of the shapes for concise indexing +S1 = [1, -9] +S2 = [-5, 3] +S3 = [-5, -5] + +lithotypes = np.zeros(M) +# a small upper triangular helper matrix to create the triangle +triu = np.triu(np.ones((rect[0], rect[0]))) +# the triangle +lithotypes[ + M[0] // 2 + S1[0] : M[0] // 2 + S1[0] + rect[0], + M[1] // 2 + S1[1] : M[1] // 2 + S1[1] + rect[0], +] = triu +# the first rectangle +lithotypes[ + M[0] // 2 + S2[0] - rect[0] // 2 : M[0] // 2 + S2[0] + rect[0] // 2, + M[1] // 2 + S2[1] - rect[1] // 2 : M[1] // 2 + S2[1] + rect[1] // 2, +] = 2 +# the second rectangle +lithotypes[ + M[0] // 2 + S3[0] - rect[0] // 2 : M[0] // 2 + S3[0] + rect[0] // 2, + M[1] // 2 + S3[1] - rect[1] // 2 : M[1] // 2 + S3[1] + rect[1] // 2, +] = 3 +# some very narrow rectangles +for i in range(4): + lithotypes[ + M[0] // 2 + S1[0] : M[0] // 2 + S1[0] + rect[0], + M[1] // 2 + S1[1] + rect[1] + 3 + 2 * i : M[1] // 2 + + S1[1] + + rect[1] + + 4 + + 2 * i, + ] = 4 + i + +############################################################################### +# With the two SRFs and the L-field ready, we can create the PGS. +pgs = gs.PGS(dim, [field1, field2]) +P = pgs(lithotypes) + +############################################################################### +# And now the plotting of the two Gaussian fields, the L-field, and the PGS. + +fig, axs = plt.subplots(2, 2) + +axs[0, 0].imshow(field1, cmap="copper", origin="lower") +axs[0, 1].imshow(field2, cmap="copper", origin="lower") +axs[1, 0].imshow(lithotypes, cmap="copper", origin="lower") +axs[1, 1].imshow(P, cmap="copper", origin="lower") +plt.show() + +############################################################################### +# We can see that the two lower light and medium brown rectangles both fill up +# large and rather smooth areas of the PGS. And they share very long common +# borders due to the fact that these categories touch each other along one of +# their edges. The next large area is the dark brown of the upper triangle. +# This category is always very close to the light brown areas, but only +# sometimes close to the medium brown areas, as they only share small parts in +# close proximity to each other. Finally, we have the four stripes. They create +# distorted stripes in the PGS. The lighter they get, the less area they fill. +# This is due to the fact that their area is not only relatively small, but +# also because they are increasingly further away from the center of the +# lithotypes. diff --git a/GSTools/source/examples/11_plurigaussian/03_correlations.py b/GSTools/source/examples/11_plurigaussian/03_correlations.py new file mode 100644 index 0000000000000000000000000000000000000000..1c55b30c55994de7aa12d7f22a765c58a7b0e106 --- /dev/null +++ b/GSTools/source/examples/11_plurigaussian/03_correlations.py @@ -0,0 +1,69 @@ +""" +Understanding the Influence of Variograms +----------------------------------------- + +Up until now, we have only used very smooth Gaussian variograms for the +underlying spatial random fields. Now, we will combine a smooth Gaussian +field with a much rougher exponential field. This example should feel +familiar, if you had a look at the previous examples. +""" + +import matplotlib.pyplot as plt +import numpy as np + +import gstools as gs + +dim = 2 +# no. of cells in both dimensions +N = [200, 200] + +x = np.arange(N[0]) +y = np.arange(N[1]) + +############################################################################### +# Now, we generate fields with a Gaussian and an Exponential variogram. + +model1 = gs.Gaussian(dim=dim, var=1, len_scale=[50, 25]) +srf1 = gs.SRF(model1) +field1 = srf1.structured([x, y], seed=20170519) +model2 = gs.Exponential(dim=dim, var=1, len_scale=[40, 40]) +srf2 = gs.SRF(model2) +field2 = srf2.structured([x, y], seed=19970221) + +############################################################################### +# The lithotypes will consist of a circle which contains one category and the +# surrounding is the second category. + +# no. of grid cells of the lithotypes +M = [200, 200] + +# radius of circle +radius = 25 + +x_lith = np.arange(M[0]) +y_lith = np.arange(M[1]) +lithotypes = np.zeros(M) +mask = (x_lith[:, np.newaxis] - M[0] // 2) ** 2 + ( + y_lith[np.newaxis, :] - M[1] // 2 +) ** 2 < radius**2 +lithotypes[mask] = 1 + +############################################################################### +# With the two SRFs and the lithotypes ready, we can create the PGS. +pgs = gs.PGS(dim, [field1, field2]) +P = pgs(lithotypes) + +############################################################################### +# And now the plotting of the two Gaussian fields, the lithotypes, and the PGS. + +fig, axs = plt.subplots(2, 2) + +axs[0, 0].imshow(field1, cmap="copper", origin="lower") +axs[0, 1].imshow(field2, cmap="copper", origin="lower") +axs[1, 0].imshow(lithotypes, cmap="copper", origin="lower") +axs[1, 1].imshow(P, cmap="copper", origin="lower") + +############################################################################### +# In this PGS, we can see two different spatial structures combined. We see large +# and rather smooth structures and shapes, which are surrounded by very rough and +# unconnected patches. diff --git a/GSTools/source/examples/11_plurigaussian/04_3d_pgs.py b/GSTools/source/examples/11_plurigaussian/04_3d_pgs.py new file mode 100644 index 0000000000000000000000000000000000000000..9457fea01025026142828040f13995f953d4759b --- /dev/null +++ b/GSTools/source/examples/11_plurigaussian/04_3d_pgs.py @@ -0,0 +1,82 @@ +""" +Creating a Three Dimensional PGS +-------------------------------- + +Let's create a 3d PGS! This will mostly feel very familiar, but the plotting +will be a bit more involved. +""" + +# sphinx_gallery_thumbnail_path = 'pics/3d_pgs.png' +import numpy as np + +import gstools as gs + +dim = 3 +# no. of cells in all dimensions +N = [40] * dim + +x = np.arange(N[0]) +y = np.arange(N[1]) +z = np.arange(N[2]) + +############################################################################### +# Because we want to create a 3d PGS, we have to generate 3 SRFs. If we are +# interested in even higher dimensions, we could solve this code repetition +# by using a loop... + +model1 = gs.Gaussian(dim=dim, var=1, len_scale=[20, 10, 15]) +srf1 = gs.SRF(model1) +field1 = srf1.structured([x, y, z], seed=20170519) +model2 = gs.Exponential(dim=dim, var=1, len_scale=[5, 5, 5]) +srf2 = gs.SRF(model2) +field2 = srf2.structured([x, y, z], seed=19970221) +model3 = gs.Gaussian(dim=dim, var=1, len_scale=[7, 12, 18]) +srf3 = gs.SRF(model3) +field3 = srf3.structured([x, y, z], seed=20011012) + +############################################################################### +# The 3d lithotypes field will consist of a cube which contains one category +# and the surrounding is the second category. + +# size of cube +cube = [18] * dim + +lithotypes = np.zeros(N) +lithotypes[ + N[0] // 2 - cube[0] // 2 : N[0] // 2 + cube[0] // 2, + N[1] // 2 - cube[1] // 2 : N[1] // 2 + cube[1] // 2, + N[2] // 2 - cube[2] // 2 : N[2] // 2 + cube[2] // 2, +] = 1 + +############################################################################### +# With the three SRFs and the lithotypes ready, we can create the 3d PGS. +pgs = gs.PGS(dim, [field1, field2, field3]) +P = pgs(lithotypes) + +# ############################################################################### +# For ploting the 3d PGS, we will use [PyVista](https://pyvista.org/) which works +# nicely together with GSTools. + +import pyvista as pv + +grid = pv.ImageData(dimensions=N) + +# uncomment, if you want to see lithotypes field, which is just a cube... +# grid.point_data['lithotypes'] = np.meshgrid(lithotypes, indexing="ij")[0] +# grid.plot(show_edges=True) + +grid.point_data["PGS"] = P.reshape(-1) + +############################################################################### +# .. note:: +# PyVista does not work on readthedocs, but you can try it out yourself by +# running the example yourself. You will get an interactive version of this +# screenshot. + +# grid.contour(isosurfaces=8).plot() + +############################################################################### +# +# .. image:: ../../pics/3d_pgs.png +# :width: 400px +# :align: center diff --git a/GSTools/source/examples/11_plurigaussian/05_conditioned.py b/GSTools/source/examples/11_plurigaussian/05_conditioned.py new file mode 100644 index 0000000000000000000000000000000000000000..32d4c5fedfca371f792b3526077f024517b60c62 --- /dev/null +++ b/GSTools/source/examples/11_plurigaussian/05_conditioned.py @@ -0,0 +1,148 @@ +""" +Creating conditioned PGS +------------------------ + +In case we have knowledge about values of the PGS in certain areas, we should +incorporate that knowledge. We can do this by using conditional fields, which +are created by combining kriged fields with SRFs. This can be done quite easily +with GSTools. For more details, have a look at the kriging and conditional +field examples. + +In this example, we assume that we know the categorical values of the PGS field +to be 2 in the lower left 20 by 20 grid cells. + +Warning: Using PGS for conditioning fields is still a beta feature. +""" + +import matplotlib.pyplot as plt +import numpy as np + +import gstools as gs + +dim = 2 +# no. of cells in both dimensions +N = [100, 80] + +# grid +x = np.arange(N[0]) +y = np.arange(N[1]) + +############################################################################### +# Now we want to read the known data in order to condition the PGS on them. +# Normally, we would probably get the data in a different format, but in order +# to keep the dependencies for this example to a minimum, we will simpy use a +# numpy format. After reading in the data, we will have a quick look at how the +# data looks like. +# We'll see the first few values, which are all 1. This value is not very +# important. However, it should be in the range of SRF values, to not mess +# with the kriging. The known value of the PGS, namely 2, will be set for the +# lithotypes field, which will map it the the conditioned SRF values of 1 at given +# positions. + +cond_data = np.load("conditional_values.npz") +cond_pos = cond_data["cond_pos"] +cond_val = cond_data["cond_val"] +print(f"first 5 conditional positions:\n{cond_pos[:, :5]}") +print(f"first 5 conditional values:\n{cond_val[:5]}") + +############################################################################### +# With the conditional values ready, we can now set up the covariance model +# for the kriging. This knowledge has to normally be inferred, but here we just +# assume that we know the convariance structure of the underlying field. +# For better visualization, we use `Simple` kriging with a mean value of 0. + +model = gs.Gaussian(dim=dim, var=1, len_scale=[10, 5], angles=np.pi / 8) +krige = gs.krige.Simple(model, cond_pos=cond_pos, cond_val=cond_val, mean=0) +cond_srf = gs.CondSRF(krige) +cond_srf.set_pos([x, y], "structured") + +############################################################################### +# Now that the conditioned field class is set up, we can generate SRFs +# conditioned on our previous knowledge. We'll do that for the two SRFs needed +# for the PGS, and then we will also set up the PGS generator. Next, we'll +# use a little helper method, which can transform the coordinates from the SRFs +# to the lithotypes field. This helps us set up the area around the conditioned value +# `cond_val`. + +field1 = cond_srf(seed=484739) +field2 = cond_srf(seed=45755894) + +pgs = gs.PGS(dim, [field1, field2]) + +M = [100, 80] + +# size of the rectangle +rect = [40, 32] + +lithotypes = np.zeros(M) +# calculate grid axes of the lithotypes field +pos_lith = pgs.calc_lithotype_axes(lithotypes.shape) +# transform conditioned SRF value to lithotypes index +pos_lith_ind = pgs.transform_coords( + lithotypes.shape, [cond_val[0], cond_val[0]] +) + +# conditioned category of 2 around the conditioned values' positions +lithotypes[ + pos_lith_ind[0] - 5 : pos_lith_ind[0] + 5, + pos_lith_ind[1] - 5 : pos_lith_ind[1] + 5, +] = 2 + +############################################################################### +# With the two SRFs and the lithotypes ready, we can create the actual PGS. + +P = pgs(lithotypes) + +############################################################################### +# Finally, we can plot the PGS, but we will also show the lithotypes and the +# two original Gaussian SRFs. We will set the colours of the SRF correlation +# scatter plot to be the sum of their respective position tuples (x+y), to get +# a feeling for which point corresponds to which position. The more blue the +# points, the smaller the sum is. We can nicely see that many blue points +# gather in the highlighted rectangle of the lithotypes where the categorical +# value of 2 is set. + +fig, axs = plt.subplots(2, 2) + +axs[0, 0].imshow(field1, cmap="copper", vmin=-3, vmax=2, origin="lower") +axs[0, 1].imshow(field2, cmap="copper", vmin=-3, vmax=2, origin="lower") +axs[1, 0].scatter( + field1.flatten(), + field2.flatten(), + s=0.1, + c=(x.reshape((len(x), 1)) + y.reshape((1, len(y))).flatten()), +) +axs[1, 0].pcolormesh( + pos_lith[0], pos_lith[1], lithotypes.T, alpha=0.3, cmap="copper" +) +axs[1, 1].imshow(P, cmap="copper", origin="lower") + +plt.tight_layout() +plt.show() + +############################################################################### +# With all this set up, we can easily create an ensemble of PGS, which conform +# to the conditional values + +seed = gs.random.MasterRNG(20170519) + +ens_no = 9 +fields1 = [] +fields2 = [] +Ps = [] +for i in range(ens_no): + fields1.append(cond_srf(seed=seed())) + fields2.append(cond_srf(seed=seed())) + pgs = gs.PGS(dim, [fields1[-1], fields2[-1]]) + Ps.append(pgs(lithotypes)) + +fig, axs = plt.subplots(3, 3) +cnt = 0 +for i in range(int(np.sqrt(ens_no))): + for j in range(int(np.sqrt(ens_no))): + axs[i, j].imshow(Ps[cnt], cmap="copper", origin="lower") + + cnt += 1 + +plt.tight_layout() +plt.show() diff --git a/GSTools/source/examples/11_plurigaussian/06_periodic.py b/GSTools/source/examples/11_plurigaussian/06_periodic.py new file mode 100644 index 0000000000000000000000000000000000000000..013be71adf097584b9e1945ddb7c1736d7918239 --- /dev/null +++ b/GSTools/source/examples/11_plurigaussian/06_periodic.py @@ -0,0 +1,76 @@ +""" +Creating PGS with periodic boundaries +------------------------------------- + +Plurigaussian fields with periodic boundaries (P-PGS) are used in various +applications, including the simulation of interactions between the landsurface +and the atmosphere, as well as the application of homogenisation theory to +porous media, e.g. [Ricketts 2024](https://doi.org/10.1007/s11242-024-02074-z). + +In this example we will use GSTools's Fourier generator to create periodic +random fields, which can in turn be used to generate P-PGS. +""" + +import matplotlib.pyplot as plt +import numpy as np + +import gstools as gs + +dim = 2 +# define the spatial grid, see the periodic random field [examples](https://geostat-framework.readthedocs.io/projects/gstools/en/latest/examples/01_random_field/08_fourier.html) +# for details. + +# domain size and periodicity +lithotypes = 200 +# no. of cells in both dimensions +N = [170, 153] + +x = np.linspace(0, lithotypes, N[0], endpoint=False) +y = np.linspace(0, lithotypes, N[1], endpoint=False) + +############################################################################### +# The parameters of the covariance model are very similar to previous examples. +# The interesting part is the SRF class. We set the `generator` to `"Fourier"`, +# which inherently generates periodic SRFs. The Fourier generator needs an +# extra parameter `period` which defines the periodicity. + +model = gs.Gaussian(dim=dim, var=0.8, len_scale=40) +srf = gs.SRF(model, generator="Fourier", period=lithotypes) +field1 = srf.structured([x, y], seed=19770319) +field2 = srf.structured([x, y], seed=19860912) + +############################################################################### +# Very similar to previous examples, we create a simple lithotypes field. + +M = [200, 160] + +# size of the rectangle +rect = [40, 32] + +lithotypes = np.zeros(M) +lithotypes[ + M[0] // 2 - rect[0] // 2 : M[0] // 2 + rect[0] // 2, + M[1] // 2 - rect[1] // 2 : M[1] // 2 + rect[1] // 2, +] = 1 + +############################################################################### +# With the two SRFs and the lithotypes ready, we can create our first P-PGS. + +pgs = gs.PGS(dim, [field1, field2]) +P = pgs(lithotypes) + +############################################################################### +# Finally, we can plot the PGS, but we will also show the lithotypes and the +# two original periodic Gaussian fields. Especially with `field1` you can +# nicely see the periodic structures in the black structure in the upper right +# corner. This transfers to the P-PGS, where you can see that the structures +# seemlessly match the opposite boundaries. + +fig, axs = plt.subplots(2, 2) + +axs[0, 0].imshow(field1, cmap="copper", origin="lower") +axs[0, 1].imshow(field2, cmap="copper", origin="lower") +axs[1, 0].imshow(lithotypes, cmap="copper", origin="lower") +axs[1, 1].imshow(P, cmap="copper", origin="lower") + +plt.show() diff --git a/GSTools/source/examples/11_plurigaussian/README.rst b/GSTools/source/examples/11_plurigaussian/README.rst new file mode 100644 index 0000000000000000000000000000000000000000..66ccafa650da220b92c11ae2ff6dc5aae7d5b1ce --- /dev/null +++ b/GSTools/source/examples/11_plurigaussian/README.rst @@ -0,0 +1,14 @@ +Plurigaussian Field Generation +============================== + +Plurigaussian field simulations (PGS) are used to simulate correlated fields +of categorical data, e.g. lithofacies, hydrofacies, soil types, or +cementitious materials. +PGS uses one spatial random field (SRF) per dimension, e.g. two SRFs, when +working with two dimensional data. Furthermore, PGS needs a field, which +describes the categorical data and its spatial relations. +This might sound more complicated then it is, as we will see in the following +examples. + +Examples +-------- diff --git a/GSTools/source/examples/11_plurigaussian/conditional_values.npz b/GSTools/source/examples/11_plurigaussian/conditional_values.npz new file mode 100644 index 0000000000000000000000000000000000000000..1a86c94a34f4b611cdfd5b9af65d9ba7c50343f3 --- /dev/null +++ b/GSTools/source/examples/11_plurigaussian/conditional_values.npz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:feac62c25bff3e94cb79f46b7db1200f2344b1ca81ac24988247b8a73f2445d3 +size 10118 diff --git a/GSTools/source/examples/12_sum_model/00_simple_sum_model.py b/GSTools/source/examples/12_sum_model/00_simple_sum_model.py new file mode 100644 index 0000000000000000000000000000000000000000..a9ded98ed9ff9c38bd58261d6c7b6f2ec5370ec9 --- /dev/null +++ b/GSTools/source/examples/12_sum_model/00_simple_sum_model.py @@ -0,0 +1,61 @@ +r""" +Creating a Sum Model +-------------------- + +This tutorial demonstrates how to create and use sum models in GSTools. +We'll combine a Spherical and a Gaussian covariance model to construct +a sum model, visualize its variogram, and generate spatial random fields. + +Let's start with importing GSTools and setting up the domain size. +""" + +import gstools as gs + +x = y = range(100) + +############################################################################### +# First, we create two individual covariance models: a :any:`Spherical` model and a +# :any:`Gaussian` model. The Spherical model with its short length scale +# will emphasize small-scale variability, while the Gaussian model with a larger length scale +# captures larger-scale patterns. + +m0 = gs.Spherical(dim=2, var=2.0, len_scale=5.0) +m1 = gs.Gaussian(dim=2, var=1.0, len_scale=10.0) + +############################################################################### +# Next, we create a sum model by adding these two models together. +# Let's visualize the resulting variogram alongside the individual models. + +model = m0 + m1 +ax = model.plot(x_max=20) +m0.plot(x_max=20, ax=ax) +m1.plot(x_max=20, ax=ax) + +############################################################################### +# As shown, the Spherical model controls the behavior at shorter distances, +# while the Gaussian model dominates at longer distances. The ratio of influence +# is thereby controlled by the provided variances of the individual models. +# +# Using the sum model, we can generate a spatial random field. Let's visualize +# the field created by the sum model. + +srf = gs.SRF(model, seed=20250107) +srf.structured((x, y)) +srf.plot() + +############################################################################### +# For comparison, we generate random fields using the individual models +# to observe their contributions more clearly. + +srf0 = gs.SRF(m0, seed=20250107) +srf0.structured((x, y)) +srf0.plot() + +srf1 = gs.SRF(m1, seed=20250107) +srf1.structured((x, y)) +srf1.plot() + +############################################################################### +# As seen, the Gaussian model introduces large-scale structures, while the +# Spherical model influences the field's roughness. The sum model combines +# these effects, resulting in a field that reflects multi-scale variability. diff --git a/GSTools/source/examples/12_sum_model/01_fitting_sum_model.py b/GSTools/source/examples/12_sum_model/01_fitting_sum_model.py new file mode 100644 index 0000000000000000000000000000000000000000..7a0dfa294d55f52ca0b5d39641f17ce2abe483d9 --- /dev/null +++ b/GSTools/source/examples/12_sum_model/01_fitting_sum_model.py @@ -0,0 +1,80 @@ +r""" +Fitting a Sum Model +-------------------- + +In this tutorial, we demonstrate how to fit a sum model consisting of two +covariance models to an empirical variogram. + +We will generate synthetic data, compute an empirical variogram, and fit a +sum model combining a Spherical and Gaussian model to it. +""" + +import gstools as gs + +x = y = range(100) + +############################################################################### +# First, we create a synthetic random field based on a known sum model. +# This will serve as the ground truth for fitting. + +# Define the true sum model +m0 = gs.Spherical(dim=2, var=2.0, len_scale=5.0) +m1 = gs.Gaussian(dim=2, var=1.0, len_scale=10.0) +true_model = m0 + m1 + +# Generate synthetic field +srf = gs.SRF(true_model, seed=20250405) +field = srf.structured((x, y)) + +############################################################################### +# Next, we calculate the empirical variogram from the synthetic data. + +# Compute empirical variogram +bin_center, gamma = gs.vario_estimate((x, y), field) + +############################################################################### +# Now we define a sum model to fit to the empirical variogram. +# Initially, the parameters of the models are arbitrary. +# +# A sum model can also be created by a list of model classes together with +# the common arguments (like dim in this case). + +fit_model = gs.SumModel(gs.Spherical, gs.Gaussian, dim=2) + +############################################################################### +# We fit the sum model to the empirical variogram using GSTools' built-in +# fitting capabilities. We deactivate the nugget fitting to not overparameterize +# our model. + +fit_model.fit_variogram(bin_center, gamma, nugget=False) +print(f"{true_model=}") +print(f" {fit_model=}") + +############################################################################### +# The variance of a sum model is the sum of the sub variances (:any:`SumModel.vars`) +# from the contained models. The length scale is a weighted sum of the sub +# length scales (:any:`SumModel.len_scales`) where the weights are the ratios +# of the sub variances to the total variance of the sum model. + +print(f"{true_model.var=:.2}, {true_model.len_scale=:.2}") +print(f" {fit_model.var=:.2}, {fit_model.len_scale=:.2}") + +############################################################################### +# After fitting, we can visualize the empirical variogram alongside the +# fitted sum model and its components. A Sum Model is subscriptable to access +# the individual models its contains. + +ax = fit_model.plot(x_max=max(bin_center)) +ax.scatter(bin_center, gamma) +# Extract individual components +fit_model[0].plot(x_max=max(bin_center), ax=ax) +fit_model[1].plot(x_max=max(bin_center), ax=ax) +# True models +true_model.plot(x_max=max(bin_center), ax=ax, ls="--", c="C0", label="") +true_model[0].plot(x_max=max(bin_center), ax=ax, ls="--", c="C1", label="") +true_model[1].plot(x_max=max(bin_center), ax=ax, ls="--", c="C2", label="") + +############################################################################### +# As we can see, the fitted sum model closely matches the empirical variogram, +# demonstrating its ability to capture multi-scale variability effectively. +# The "true" variograms are shown with dashed lines for comparison. diff --git a/GSTools/source/examples/12_sum_model/README.rst b/GSTools/source/examples/12_sum_model/README.rst new file mode 100644 index 0000000000000000000000000000000000000000..c4ccb23064b6e2269f8982f6bab0af587128c660 --- /dev/null +++ b/GSTools/source/examples/12_sum_model/README.rst @@ -0,0 +1,30 @@ +Summing Covariance Models +========================= + +In geostatistics, the spatial relations of natural phenomena is often represented using covariance models, +which describe how values of a property correlate over distance. +A single covariance model may capture specific features of the spatial correlation, such as smoothness or the range of influence. +However, many real-world spatial processes are complex, involving multiple overlapping structures +that cannot be adequately described by a single covariance model. + +This is where **sum models** come into play. +A sum model combines multiple covariance models into a single representation, +allowing for a more flexible and comprehensive description of spatial variability. +By summing covariance models, we can: + +1. **Capture Multi-Scale Variability:** Many spatial phenomena exhibit variability at different scales. + For example, soil properties may have small-scale variation due to local heterogeneities and large-scale variation due to regional trends. + A sum model can combine short-range and long-range covariance models to reflect this behavior. +2. **Improve Model Fit and Prediction Accuracy:** By combining models, sum models can better match empirical variograms or other observed data, + leading to more accurate predictions in kriging or simulation tasks. +3. **Enhance Interpretability:** Each component of a sum model can be associated with a specific spatial process or scale, + providing insights into the underlying mechanisms driving spatial variability. + +The new :any:`SumModel` introduced in GSTools makes it straightforward to define and work with such composite covariance structures. +It allows users to combine any number of base models, each with its own parameters, in a way that is both intuitive and computationally efficient. + +In the following tutorials, we'll explore how to use the :any:`SumModel` in GSTools, +including practical examples that demonstrate its utility in different scenarios. + +Examples +-------- diff --git a/GSTools/source/pyproject.toml b/GSTools/source/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..885bcd774dec4d63144fbc911b10a67762f6f35e --- /dev/null +++ b/GSTools/source/pyproject.toml @@ -0,0 +1,119 @@ + + +[build-system] +requires = ["hatchling>=1.8.0", "hatch-vcs"] +build-backend = "hatchling.build" + +[project] +requires-python = ">=3.8" +name = "gstools" +description = "GSTools: A geostatistical toolbox." +authors = [ + { name = "Sebastian Müller, Lennart Schüler", email = "info@geostat-framework.org" }, +] +readme = "README.md" +license = "LGPL-3.0" +dynamic = ["version"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: End Users/Desktop", + "Intended Audience :: Science/Research", + "Intended Audience :: Education", + "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", + "Natural Language :: English", + "Operating System :: Unix", + "Operating System :: Microsoft", + "Operating System :: MacOS", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: GIS", + "Topic :: Scientific/Engineering :: Hydrology", + "Topic :: Scientific/Engineering :: Mathematics", + "Topic :: Scientific/Engineering :: Physics", + "Topic :: Utilities", +] +dependencies = [ + "gstools-cython>=1,<2", + "emcee>=3.0.0", + "hankel>=1.0.0", + "meshio>=5.1.0", + "numpy>=1.20.0", + "pyevtk>=1.1.1", + "scipy>=1.1.0", +] + +[project.optional-dependencies] +doc = [ + "myst_parser", + "matplotlib>=3.7", + "meshzoo>=0.7", + "numpydoc>=1.1", + "pykrige>=1.5,<2", + "pyvista>=0.40", + "sphinx>=7", + "sphinx-gallery>=0.8", + "sphinx-rtd-theme>=3", + "sphinxcontrib-youtube>=1.1", +] +plotting = ["matplotlib>=3.7", "pyvista>=0.40"] +rust = ["gstools_core>=1.0.0"] +test = ["pytest-cov>=3"] +lint = ["ruff"] + +[project.urls] +Changelog = "https://github.com/GeoStat-Framework/GSTools/blob/main/CHANGELOG.md" +Conda-Forge = "https://anaconda.org/conda-forge/gstools" +Documentation = "https://gstools.readthedocs.io" +Homepage = "https://geostat-framework.org/#gstools" +Source = "https://github.com/GeoStat-Framework/GSTools" +Tracker = "https://github.com/GeoStat-Framework/GSTools/issues" + +[tool.hatch.version] +source = "vcs" +fallback_version = "0.0.0.dev0" + +[tool.hatch.version.raw-options] +local_scheme = "no-local-version" + +[tool.hatch.build.hooks.vcs] +version-file = "src/gstools/_version.py" +template = "__version__ = '{version}'" + +[tool.hatch.build.targets.sdist] +include = ["/src", "/tests"] + +[tool.hatch.build.targets.wheel] +packages = ["src/gstools"] + +[tool.ruff] +line-length = 79 +target-version = "py38" + +[tool.ruff.lint] +select = [ + "F", + "I", +] + +[tool.coverage] +[tool.coverage.run] +source = ["gstools"] +omit = [ + "*docs*", + "*examples*", + "*tests*", + "*/src/gstools/covmodel/plot.py", + "*/src/gstools/field/plot.py", +] + +[tool.coverage.report] +exclude_lines = ["pragma: no cover", "def __repr__", "def __str__"] diff --git a/GSTools/source/src/__init__.py b/GSTools/source/src/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e6e237ed8dcb6e6cc47eed4854b52979143e6191 --- /dev/null +++ b/GSTools/source/src/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +""" +src Package Initialization File +""" diff --git a/GSTools/source/src/gstools/__init__.py b/GSTools/source/src/gstools/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4d12007c92406d5208e073af495e76890533e98e --- /dev/null +++ b/GSTools/source/src/gstools/__init__.py @@ -0,0 +1,253 @@ +""" +Purpose +======= + +GSTools is a library providing geostatistical tools +for random field generation, conditioned field generation, +kriging and variogram estimation +based on a list of provided or even user-defined covariance models. + +The following functionalities are directly provided on module-level. + +Subpackages +=========== + +.. autosummary:: + :toctree: api + + covmodel + field + variogram + krige + random + tools + transform + normalizer + +Classes +======= + +Kriging +^^^^^^^ +Swiss-Army-Knife for Kriging. For short cut classes see: :any:`gstools.krige` + +.. currentmodule:: gstools.krige + +.. autosummary:: + Krige + +Spatial Random Field +^^^^^^^^^^^^^^^^^^^^ +Classes for (conditioned) random field generation + +.. currentmodule:: gstools.field + +.. autosummary:: + SRF + CondSRF + PGS + +Covariance Base-Class +^^^^^^^^^^^^^^^^^^^^^ +Class to construct user defined covariance models + +.. currentmodule:: gstools.covmodel + +.. autosummary:: + CovModel + SumModel + +Covariance Models +^^^^^^^^^^^^^^^^^ + +Standard Covariance Models +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autosummary:: + Nugget + Gaussian + Exponential + Matern + Integral + Stable + Rational + Cubic + Linear + Circular + Spherical + HyperSpherical + SuperSpherical + JBessel + +Truncated Power Law Covariance Models +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autosummary:: + TPLGaussian + TPLExponential + TPLStable + TPLSimple + +Functions +========= + +VTK-Export +^^^^^^^^^^ +Routines to export fields to the vtk format + +.. currentmodule:: gstools.tools + +.. autosummary:: + vtk_export + to_vtk + +Geometric +^^^^^^^^^ +Some convenient functions for geometric operations + +.. autosummary:: + rotated_main_axes + generate_grid + generate_st_grid + +Variogram Estimation +^^^^^^^^^^^^^^^^^^^^ +Estimate the variogram of a given field with these routines + +.. currentmodule:: gstools.variogram + +.. autosummary:: + vario_estimate + vario_estimate_axis + standard_bins + +Misc +==== + +.. currentmodule:: gstools.tools + +.. autosummary:: + EARTH_RADIUS + KM_SCALE + DEGREE_SCALE + RADIAN_SCALE +""" + +# Hooray! +from gstools import ( + config, + covmodel, + field, + krige, + normalizer, + random, + tools, + transform, + variogram, +) +from gstools.covmodel import ( + Circular, + CovModel, + Cubic, + Exponential, + Gaussian, + HyperSpherical, + Integral, + JBessel, + Linear, + Matern, + Nugget, + Rational, + Spherical, + Stable, + SumModel, + SuperSpherical, + TPLExponential, + TPLGaussian, + TPLSimple, + TPLStable, +) +from gstools.field import PGS, SRF, CondSRF +from gstools.krige import Krige +from gstools.tools import ( + DEGREE_SCALE, + EARTH_RADIUS, + KM_SCALE, + RADIAN_SCALE, + generate_grid, + generate_st_grid, + rotated_main_axes, + to_vtk, + to_vtk_structured, + to_vtk_unstructured, + vtk_export, + vtk_export_structured, + vtk_export_unstructured, +) +from gstools.variogram import ( + standard_bins, + vario_estimate, + vario_estimate_axis, + vario_estimate_structured, + vario_estimate_unstructured, +) + +try: + from gstools._version import __version__ +except ModuleNotFoundError: # pragma: no cover + # package is not installed + __version__ = "0.0.0.dev0" + +__all__ = ["__version__"] +__all__ += ["covmodel", "field", "variogram", "krige", "random", "tools"] +__all__ += ["transform", "normalizer", "config"] +__all__ += [ + "CovModel", + "SumModel", + "Nugget", + "Gaussian", + "Exponential", + "Matern", + "Integral", + "Stable", + "Rational", + "Cubic", + "Linear", + "Circular", + "Spherical", + "HyperSpherical", + "SuperSpherical", + "JBessel", + "TPLGaussian", + "TPLExponential", + "TPLStable", + "TPLSimple", +] + +__all__ += [ + "vario_estimate", + "vario_estimate_axis", + "vario_estimate_structured", + "vario_estimate_unstructured", + "standard_bins", +] + +__all__ += [ + "Krige", + "SRF", + "CondSRF", + "PGS", + "rotated_main_axes", + "generate_grid", + "generate_st_grid", + "EARTH_RADIUS", + "KM_SCALE", + "DEGREE_SCALE", + "RADIAN_SCALE", + "vtk_export", + "vtk_export_structured", + "vtk_export_unstructured", + "to_vtk", + "to_vtk_structured", + "to_vtk_unstructured", +] diff --git a/GSTools/source/src/gstools/config.py b/GSTools/source/src/gstools/config.py new file mode 100644 index 0000000000000000000000000000000000000000..3870ac1e1b73d381119e92ee39b0924a2c05e44e --- /dev/null +++ b/GSTools/source/src/gstools/config.py @@ -0,0 +1,17 @@ +""" +GStools subpackage providing global variables. + +.. currentmodule:: gstools.config + +""" + +NUM_THREADS = None + +try: # pragma: no cover + import gstools_core # noqa: F401 + + _GSTOOLS_CORE_AVAIL = True + USE_GSTOOLS_CORE = True +except ImportError: + _GSTOOLS_CORE_AVAIL = False + USE_GSTOOLS_CORE = False diff --git a/GSTools/source/src/gstools/covmodel/__init__.py b/GSTools/source/src/gstools/covmodel/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..76920c61e6186ff1abebdb37b9fa386ed421fef9 --- /dev/null +++ b/GSTools/source/src/gstools/covmodel/__init__.py @@ -0,0 +1,103 @@ +""" +GStools subpackage providing a set of handy covariance models. + +.. currentmodule:: gstools.covmodel + +Subpackages +^^^^^^^^^^^ + +.. autosummary:: + :toctree: + + plot + +Covariance Base-Class +^^^^^^^^^^^^^^^^^^^^^ +Class to construct user defined covariance models + +.. autosummary:: + :toctree: + + CovModel + SumModel + +Covariance Models +^^^^^^^^^^^^^^^^^ +Standard Covariance Models + +.. autosummary:: + :toctree: + + Nugget + Gaussian + Exponential + Matern + Integral + Stable + Rational + Cubic + Linear + Circular + Spherical + HyperSpherical + SuperSpherical + JBessel + +Truncated Power Law Covariance Models +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autosummary:: + :toctree: + + TPLGaussian + TPLExponential + TPLStable + TPLSimple +""" + +from gstools.covmodel.base import CovModel, SumModel +from gstools.covmodel.models import ( + Circular, + Cubic, + Exponential, + Gaussian, + HyperSpherical, + Integral, + JBessel, + Linear, + Matern, + Nugget, + Rational, + Spherical, + Stable, + SuperSpherical, +) +from gstools.covmodel.tpl_models import ( + TPLExponential, + TPLGaussian, + TPLSimple, + TPLStable, +) + +__all__ = [ + "CovModel", + "SumModel", + "Nugget", + "Gaussian", + "Exponential", + "Matern", + "Integral", + "Stable", + "Rational", + "Cubic", + "Linear", + "Circular", + "Spherical", + "HyperSpherical", + "SuperSpherical", + "JBessel", + "TPLGaussian", + "TPLExponential", + "TPLStable", + "TPLSimple", +] diff --git a/GSTools/source/src/gstools/covmodel/base.py b/GSTools/source/src/gstools/covmodel/base.py new file mode 100644 index 0000000000000000000000000000000000000000..8e3b2ae3f0e4b5b401171e8b686342267804d10c --- /dev/null +++ b/GSTools/source/src/gstools/covmodel/base.py @@ -0,0 +1,1767 @@ +""" +GStools subpackage providing the base class for covariance models. + +.. currentmodule:: gstools.covmodel.base + +The following classes are provided + +.. autosummary:: + CovModel + SumModel +""" + +import copy + +import numpy as np +from hankel import SymmetricFourierTransform as SFT +from scipy.integrate import quad as integral + +from gstools.covmodel import plot +from gstools.covmodel.fit import fit_variogram +from gstools.covmodel.sum_tools import ( + default_mod_kwargs, + sum_check, + sum_compare, + sum_default_arg_bounds, + sum_default_opt_arg_bounds, + sum_model_repr, + sum_set_len_weights, + sum_set_var_weights, +) +from gstools.covmodel.tools import ( + _init_subclass, + check_arg_bounds, + check_arg_in_bounds, + compare, + default_arg_from_bounds, + model_repr, + percentile_scale, + set_arg_bounds, + set_dim, + set_len_anis, + set_model_angles, + set_opt_args, + spectral_rad_pdf, +) +from gstools.tools import RADIAN_SCALE +from gstools.tools.geometric import ( + great_circle_to_chordal, + latlon2pos, + matrix_anisometrize, + matrix_isometrize, + pos2latlon, + rotated_main_axes, +) + +__all__ = ["CovModel"] + +# default arguments for hankel.SymmetricFourierTransform +HANKEL_DEFAULT = {"a": -1, "b": 1, "N": 200, "h": 0.001, "alt": True} + + +class CovModel: + r"""Base class for the GSTools covariance models. + + Parameters + ---------- + dim : :class:`int`, optional + dimension of the model. + Includes the temporal dimension if temporal is true. + To specify only the spatial dimension in that case, use `spatial_dim`. + Default: ``3`` + var : :class:`float`, optional + variance of the model (the nugget is not included in "this" variance) + Default: ``1.0`` + len_scale : :class:`float` or :class:`list`, optional + length scale of the model. + If a single value is given, the same length-scale will be used for + every direction. If multiple values (for main and transversal + directions) are given, `anis` will be + recalculated accordingly. If only two values are given in 3D, + the latter one will be used for both transversal directions. + Default: ``1.0`` + nugget : :class:`float`, optional + nugget of the model. Default: ``0.0`` + anis : :class:`float` or :class:`list`, optional + anisotropy ratios in the transversal directions [e_y, e_z]. + + * e_y = l_y / l_x + * e_z = l_z / l_x + + If only one value is given in 3D, e_y will be set to 1. + This value will be ignored, if multiple len_scales are given. + Default: ``1.0`` + angles : :class:`float` or :class:`list`, optional + angles of rotation (given in rad): + + * in 2D: given as rotation around z-axis + * in 3D: given by yaw, pitch, and roll (known as Tait–Bryan angles) + + Default: ``0.0`` + integral_scale : :class:`float` or :class:`list` or :any:`None`, optional + If given, ``len_scale`` will be ignored and recalculated, + so that the integral scale of the model matches the given one. + Default: :any:`None` + rescale : :class:`float` or :any:`None`, optional + Optional rescaling factor to divide the length scale with. + This could be used for unit conversion or rescaling the length scale + to coincide with e.g. the integral scale. + Will be set by each model individually. + Default: :any:`None` + latlon : :class:`bool`, optional + Whether the model is describing 2D fields on earths surface described + by latitude and longitude. When using this, the model will internally + use the associated 'Yadrenko' model to represent a valid model. + This means, the spatial distance :math:`r` will be replaced by + :math:`2\sin(\alpha/2)`, where :math:`\alpha` is the great-circle + distance, which is equal to the spatial distance of two points in 3D. + As a consequence, `dim` will be set to `3` and anisotropy will be + disabled. `geo_scale` can be set to e.g. earth's radius, + to have a meaningful `len_scale` parameter. + Default: False + geo_scale : :class:`float`, optional + Geographic unit scaling in case of latlon coordinates to get a + meaningful length scale unit. + By default, len_scale is assumed to be in radians with latlon=True. + Can be set to :any:`KM_SCALE` to have len_scale in km or + :any:`DEGREE_SCALE` to have len_scale in degrees. + Default: :any:`RADIAN_SCALE` + temporal : :class:`bool`, optional + Create a metric spatio-temporal covariance model. + Setting this to true will increase `dim` and `field_dim` by 1. + `spatial_dim` will be `field_dim - 1`. + The time-dimension is appended, meaning the pos tuple is (x,y,z,...,t). + Default: False + spatial_dim : :class:`int`, optional + spatial dimension of the model. + If given, the model dimension will be determined from this spatial dimension + and the possible temporal dimension if temporal is ture. + Default: None + hankel_kw: :class:`dict` or :any:`None`, optional + Modify the init-arguments of + :any:`hankel.SymmetricFourierTransform` + used for the spectrum calculation. Use with caution (Better: Don't!). + ``None`` is equivalent to ``{"a": -1, "b": 1, "N": 1000, "h": 0.001}``. + Default: :any:`None` + **opt_arg + Optional arguments are covered by these keyword arguments. + If present, they are described in the section `Other Parameters`. + """ + + _add_doc = True + """Flag to append the above doc-string about parameters to the model implementation.""" + + def __init__( + self, + dim=3, + var=1.0, + len_scale=1.0, + nugget=0.0, + anis=1.0, + angles=0.0, + *, + integral_scale=None, + rescale=None, + latlon=False, + geo_scale=RADIAN_SCALE, + temporal=False, + spatial_dim=None, + hankel_kw=None, + **opt_arg, + ): + # assert, that we use a subclass + # this is the case, if __init_subclass__ is called, which creates + # the "variogram"... so we check for that + if not hasattr(self, "variogram"): + raise TypeError("Don't instantiate 'CovModel' directly!") + + # indicator for initialization status (True after __init__) + self._init = False + # prepare dim setting + self._dim = None + self._hankel_kw = None + self._sft = None + # prepare parameters (they are checked in dim setting) + self._rescale = None + self._len_scale = None + self._anis = None + self._angles = None + # prepare parameters boundaries + self._var_bounds = None + self._len_scale_bounds = None + self._nugget_bounds = None + self._anis_bounds = None + self._opt_arg_bounds = {} + # Set latlon and temporal first + self._latlon = bool(latlon) + self._temporal = bool(temporal) + self._geo_scale = abs(float(geo_scale)) + # SFT class will be created within dim.setter but needs hankel_kw + self.hankel_kw = hankel_kw + # using time increases model dimension given by "spatial_dim" + self.dim = ( + dim if spatial_dim is None else spatial_dim + int(self.temporal) + ) + + # optional arguments for the variogram-model + set_opt_args(self, opt_arg) + + # set standard boundaries for variance, len_scale, nugget and opt_arg + bounds = self.default_arg_bounds() + bounds.update(self.default_opt_arg_bounds()) + self.set_arg_bounds(check_args=False, **bounds) + + # set parameters + self.rescale = rescale + self._var = float(var) + self._nugget = float(nugget) + + # set anisotropy and len_scale, disable anisotropy for latlon models + if integral_scale is not None: + len_scale = integral_scale + self._len_scale, self._anis = set_len_anis( + self.dim, len_scale, anis, self.latlon + ) + self._angles = set_model_angles( + self.dim, angles, self.latlon, self.temporal + ) + + if integral_scale is not None: + self.integral_scale = integral_scale + + # final check for parameter bounds + self.check_arg_bounds() + # additional checks for the optional arguments (provided by user) + self.check_opt_arg() + # precision for printing + self._prec = 3 + # initialized + self._init = True + + # one of these functions needs to be overridden + def __init_subclass__(cls): + """Initialize gstools covariance model.""" + _init_subclass(cls) + + # modify the docstrings: class docstring gets attributes added + if cls.__doc__ is None: + cls.__doc__ = "User defined GSTools Covariance-Model." + if cls._add_doc: + cls.__doc__ += CovModel.__doc__[45:] + # overridden functions get standard doc if no new doc was created + ign = ["__", "variogram", "covariance", "cor"] + for att, attr_cls in cls.__dict__.items(): + if any(att.startswith(i) for i in ign) or att not in dir(CovModel): + continue + attr_doc = getattr(CovModel, att).__doc__ + if attr_cls.__doc__ is None: + attr_cls.__doc__ = attr_doc + + # special variogram functions + + def vario_axis(self, r, axis=0): + r"""Variogram along axis of anisotropy.""" + if axis == 0: + return self.variogram(r) + return self.variogram(np.abs(r) / self.anis[axis - 1]) + + def cov_axis(self, r, axis=0): + r"""Covariance along axis of anisotropy.""" + if axis == 0: + return self.covariance(r) + return self.covariance(np.abs(r) / self.anis[axis - 1]) + + def cor_axis(self, r, axis=0): + r"""Correlation along axis of anisotropy.""" + if axis == 0: + return self.correlation(r) + return self.correlation(np.abs(r) / self.anis[axis - 1]) + + def vario_yadrenko(self, zeta): + r"""Yadrenko variogram for great-circle distance from latlon-pos.""" + return self.variogram(great_circle_to_chordal(zeta, self.geo_scale)) + + def cov_yadrenko(self, zeta): + r"""Yadrenko covariance for great-circle distance from latlon-pos.""" + return self.covariance(great_circle_to_chordal(zeta, self.geo_scale)) + + def cor_yadrenko(self, zeta): + r"""Yadrenko correlation for great-circle distance from latlon-pos.""" + return self.correlation(great_circle_to_chordal(zeta, self.geo_scale)) + + def vario_spatial(self, pos): + r"""Spatial variogram respecting anisotropy and rotation.""" + return self.variogram(self._get_iso_rad(pos)) + + def cov_spatial(self, pos): + r"""Spatial covariance respecting anisotropy and rotation.""" + return self.covariance(self._get_iso_rad(pos)) + + def cor_spatial(self, pos): + r"""Spatial correlation respecting anisotropy and rotation.""" + return self.correlation(self._get_iso_rad(pos)) + + def vario_nugget(self, r): + """Isotropic variogram of the model respecting the nugget at r=0.""" + r = np.asarray(np.abs(r), dtype=np.double) + r_gz = np.logical_not(np.isclose(r, 0)) + res = np.empty_like(r, dtype=np.double) + res[r_gz] = self.variogram(r[r_gz]) + res[np.logical_not(r_gz)] = 0.0 + return res + + def cov_nugget(self, r): + """Isotropic covariance of the model respecting the nugget at r=0.""" + r = np.asarray(np.abs(r), dtype=np.double) + r_gz = np.logical_not(np.isclose(r, 0)) + res = np.empty_like(r, dtype=np.double) + res[r_gz] = self.covariance(r[r_gz]) + res[np.logical_not(r_gz)] = self.sill + return res + + def plot(self, func="variogram", **kwargs): # pragma: no cover + """ + Plot a function of a the CovModel. + + Parameters + ---------- + func : :class:`str`, optional + Function to be plotted. Could be one of: + + * "variogram" + * "covariance" + * "correlation" + * "vario_spatial" + * "cov_spatial" + * "cor_spatial" + * "vario_yadrenko" + * "cov_yadrenko" + * "cor_yadrenko" + * "vario_axis" + * "cov_axis" + * "cor_axis" + * "spectrum" + * "spectral_density" + * "spectral_rad_pdf" + + **kwargs + Keyword arguments forwarded to the plotting function + `"plot_" + func` in :py:mod:`gstools.covmodel.plot`. + + See Also + -------- + gstools.covmodel.plot + """ + routine = getattr(plot, "plot_" + func) + return routine(self, **kwargs) + + # pykrige functions + + def pykrige_vario(self, args=None, r=0): # pragma: no cover + """Isotropic variogram of the model for pykrige.""" + if self.latlon: + return self.vario_yadrenko(np.deg2rad(r)) + return self.variogram(r) + + @property + def pykrige_anis(self): + """2D anisotropy ratio for pykrige.""" + if self.dim == 2: + return 1 / self.anis[0] + return 1.0 # pragma: no cover + + @property + def pykrige_anis_y(self): + """3D anisotropy ratio in y direction for pykrige.""" + if self.dim >= 2: + return 1 / self.anis[0] + return 1.0 # pragma: no cover + + @property + def pykrige_anis_z(self): + """3D anisotropy ratio in z direction for pykrige.""" + if self.dim == 3: + return 1 / self.anis[1] + return 1.0 # pragma: no cover + + @property + def pykrige_angle(self): + """2D rotation angle for pykrige.""" + if self.dim == 2: + return self.angles[0] / np.pi * 180 + return 0.0 # pragma: no cover + + @property + def pykrige_angle_z(self): + """3D rotation angle around z for pykrige.""" + if self.dim >= 2: + return self.angles[0] / np.pi * 180 + return 0.0 # pragma: no cover + + @property + def pykrige_angle_y(self): + """3D rotation angle around y for pykrige.""" + if self.dim == 3: + return self.angles[1] / np.pi * 180 + return 0.0 # pragma: no cover + + @property + def pykrige_angle_x(self): + """3D rotation angle around x for pykrige.""" + if self.dim == 3: + return self.angles[2] / np.pi * 180 + return 0.0 # pragma: no cover + + @property + def pykrige_kwargs(self): + """Keyword arguments for pykrige routines.""" + kwargs = { + "variogram_model": "custom", + "variogram_parameters": [], + "variogram_function": self.pykrige_vario, + } + if self.dim == 1: + add_kwargs = {} + elif self.dim == 2: + add_kwargs = { + "anisotropy_scaling": self.pykrige_anis, + "anisotropy_angle": self.pykrige_angle, + } + else: + add_kwargs = { + "anisotropy_scaling_y": self.pykrige_anis_y, + "anisotropy_scaling_z": self.pykrige_anis_z, + "anisotropy_angle_x": self.pykrige_angle_x, + "anisotropy_angle_y": self.pykrige_angle_y, + "anisotropy_angle_z": self.pykrige_angle_z, + } + kwargs.update(add_kwargs) + return kwargs + + # methods for optional/default arguments (can be overridden) + + def default_opt_arg(self): + """Provide default optional arguments by the user. + + Should be given as a dictionary when overridden. + """ + return { + opt: default_arg_from_bounds(bnd) + for (opt, bnd) in self.default_opt_arg_bounds().items() + } + + def default_opt_arg_bounds(self): + """Provide default boundaries for optional arguments.""" + res = {} + for opt in self.opt_arg: + res[opt] = [-np.inf, np.inf] + return res + + def check_opt_arg(self): + """Run checks for the optional arguments. + + This is in addition to the bound-checks + + Notes + ----- + * You can use this to raise a ValueError/warning + * Any return value will be ignored + * This method will only be run once, when the class is initialized + """ + + def check_dim(self, dim): + """Check the given dimension.""" + return True + + def fix_dim(self): + """Set a fix dimension for the model.""" + return None + + def default_rescale(self): + """Provide default rescaling factor.""" + return 1.0 + + # calculation of different scales + + def calc_integral_scale(self): + """Calculate the integral scale of the isotrope model.""" + return integral(self.correlation, 0, np.inf)[0] + + def percentile_scale(self, per=0.9): + """Calculate the percentile scale of the isotrope model. + + This is the distance, where the given percentile of the variance + is reached by the variogram + """ + return percentile_scale(self, per) + + # spectrum methods (can be overridden for speedup) + + @property + def needs_fourier_transform(self): + """ + Flag whether the model needs a fourier transform to calculate + the spectral density from the covariance function or if + it implements an analytical spectral density. + """ + base_method = getattr(CovModel, "spectral_density") + instance_method = getattr(self.__class__, "spectral_density") + return base_method == instance_method + + def spectrum(self, k): + r""" + Spectrum of the covariance model. + + This is given by: + + .. math:: S(\mathbf{k}) = \left(\frac{1}{2\pi}\right)^n + \int C(r) e^{i \mathbf{k}\cdot\mathbf{r}} d^n\mathbf{r} + + Internally, this is calculated by the hankel transformation: + + .. math:: S(k) = \left(\frac{1}{2\pi}\right)^n \cdot + \frac{(2\pi)^{n/2}}{k^{n/2-1}} + \int_0^\infty r^{n/2} C(r) J_{n/2-1}(kr) dr + + Where :math:`C(r)` is the covariance function of the model. + + Parameters + ---------- + k : :class:`float` + Radius of the phase: :math:`k=\left\Vert\mathbf{k}\right\Vert` + """ + return self.spectral_density(k) * self.var + + def spectral_density(self, k): + r""" + Spectral density of the covariance model. + + This is given by: + + .. math:: \tilde{S}(k) = \frac{S(k)}{\sigma^2} + + Where :math:`S(k)` is the spectrum of the covariance model. + + Parameters + ---------- + k : :class:`float` + Radius of the phase: :math:`k=\left\Vert\mathbf{k}\right\Vert` + """ + k = np.asarray(np.abs(k), dtype=np.double) + return self._sft.transform(self.correlation, k, ret_err=False) + + def spectral_rad_pdf(self, r): + """Radial spectral density of the model.""" + return spectral_rad_pdf(self, r) + + def ln_spectral_rad_pdf(self, r): + """Log radial spectral density of the model.""" + with np.errstate(divide="ignore"): + return np.log(self.spectral_rad_pdf(r)) + + def _has_cdf(self): + """State if a cdf is defined with 'spectral_rad_cdf'.""" + return hasattr(self, "spectral_rad_cdf") + + def _has_ppf(self): + """State if a ppf is defined with 'spectral_rad_ppf'.""" + return hasattr(self, "spectral_rad_ppf") + + # spatial routines + + def isometrize(self, pos): + """Make a position tuple ready for isotropic operations.""" + pos = np.asarray(pos, dtype=np.double).reshape((self.field_dim, -1)) + if self.latlon: + return latlon2pos( + pos, + radius=self.geo_scale, + temporal=self.temporal, + time_scale=self.anis[-1], + ) + return np.dot(matrix_isometrize(self.dim, self.angles, self.anis), pos) + + def anisometrize(self, pos): + """Bring a position tuple into the anisotropic coordinate-system.""" + pos = np.asarray(pos, dtype=np.double).reshape((self.dim, -1)) + if self.latlon: + return pos2latlon( + pos, + radius=self.geo_scale, + temporal=self.temporal, + time_scale=self.anis[-1], + ) + return np.dot( + matrix_anisometrize(self.dim, self.angles, self.anis), pos + ) + + def main_axes(self): + """Axes of the rotated coordinate-system.""" + return rotated_main_axes(self.dim, self.angles) + + def _get_iso_rad(self, pos): + """Isometrized radians.""" + pos = np.asarray(pos, dtype=np.double).reshape((self.dim, -1)) + iso = np.dot(matrix_isometrize(self.dim, self.angles, self.anis), pos) + return np.linalg.norm(iso, axis=0) + + # fitting routine + + def fit_variogram( + self, + x_data, + y_data, + anis=True, + sill=None, + init_guess="default", + weights=None, + method="trf", + loss="soft_l1", + max_eval=None, + return_r2=False, + curve_fit_kwargs=None, + **para_select, + ): + """ + Fitting the variogram-model to an empirical variogram. + + Parameters + ---------- + x_data : :class:`numpy.ndarray` + The bin-centers of the empirical variogram. + y_data : :class:`numpy.ndarray` + The measured variogram + If multiple are given, they are interpreted as the directional + variograms along the main axis of the associated rotated + coordinate system. + Anisotropy ratios will be estimated in that case. + anis : :class:`bool`, optional + In case of a directional variogram, you can control anisotropy + by this argument. Deselect the parameter from fitting, by setting + it "False". + You could also pass a fixed value to be set in the model. + Then the anisotropy ratios wont be altered during fitting. + Default: True + sill : :class:`float` or :class:`bool`, optional + Here you can provide a fixed sill for the variogram. + It needs to be in a fitting range for the var and nugget bounds. + If variance or nugget are not selected for estimation, + the nugget will be recalculated to fulfill: + + * sill = var + nugget + * if the variance is bigger than the sill, + nugget will bet set to its lower bound + and the variance will be set to the fitting partial sill. + + If variance is deselected, it needs to be less than the sill, + otherwise a ValueError comes up. Same for nugget. + If sill=False, it will be deselected from estimation + and set to the current sill of the model. + Then, the procedure above is applied. + Default: None + init_guess : :class:`str` or :class:`dict`, optional + Initial guess for the estimation. Either: + + * "default": using the default values of the covariance model + ("len_scale" will be mean of given bin centers; + "var" and "nugget" will be mean of given variogram values + (if in given bounds)) + * "current": using the current values of the covariance model + * dict: dictionary with parameter names and given value + (separate "default" can bet set to "default" or "current" for + unspecified values to get same behavior as given above + ("default" by default)) + Example: ``{"len_scale": 10, "default": "current"}`` + + Default: "default" + weights : :class:`str`, :class:`numpy.ndarray`, :class:`callable`, optional + Weights applied to each point in the estimation. Either: + + * 'inv': inverse distance ``1 / (x_data + 1)`` + * list: weights given per bin + * callable: function applied to x_data + + If callable, it must take a 1-d ndarray. + Then ``weights = f(x_data)``. + Default: None + method : {'trf', 'dogbox'}, optional + Algorithm to perform minimization. + + * 'trf' : Trust Region Reflective algorithm, + particularly suitable for large sparse problems with bounds. + Generally robust method. + * 'dogbox' : dogleg algorithm with rectangular trust regions, + typical use case is small problems with bounds. + Not recommended for problems with rank-deficient Jacobian. + + Default: 'trf' + loss : :class:`str` or :class:`callable`, optional + Determines the loss function in scipys curve_fit. + The following keyword values are allowed: + + * 'linear' (default) : ``rho(z) = z``. Gives a standard + least-squares problem. + * 'soft_l1' : ``rho(z) = 2 * ((1 + z)**0.5 - 1)``. The smooth + approximation of l1 (absolute value) loss. Usually a good + choice for robust least squares. + * 'huber' : ``rho(z) = z if z <= 1 else 2*z**0.5 - 1``. Works + similarly to 'soft_l1'. + * 'cauchy' : ``rho(z) = ln(1 + z)``. Severely weakens outliers + influence, but may cause difficulties in optimization process. + * 'arctan' : ``rho(z) = arctan(z)``. Limits a maximum loss on + a single residual, has properties similar to 'cauchy'. + + If callable, it must take a 1-d ndarray ``z=f**2`` and return an + array_like with shape (3, m) where row 0 contains function values, + row 1 contains first derivatives and row 2 contains second + derivatives. Default: 'soft_l1' + max_eval : :class:`int` or :any:`None`, optional + Maximum number of function evaluations before the termination. + If None (default), the value is chosen automatically: 100 * n. + return_r2 : :class:`bool`, optional + Whether to return the r2 score of the estimation. + Default: False + curve_fit_kwargs : :class:`dict`, optional + Other keyword arguments passed to scipys curve_fit. Default: None + **para_select + You can deselect parameters from fitting, by setting + them "False" using their names as keywords. + You could also pass fixed values for each parameter. + Then these values will be applied and the involved parameters wont + be fitted. + By default, all parameters are fitted. + + Returns + ------- + fit_para : :class:`dict` + Dictionary with the fitted parameter values + pcov : :class:`numpy.ndarray` + The estimated covariance of `popt` from + :any:`scipy.optimize.curve_fit`. + To compute one standard deviation errors + on the parameters use ``perr = np.sqrt(np.diag(pcov))``. + r2_score : :class:`float`, optional + r2 score of the curve fitting results. Only if return_r2 is True. + + Notes + ----- + You can set the bounds for each parameter by accessing + :any:`CovModel.set_arg_bounds`. + + The fitted parameters will be instantly set in the model. + """ + return fit_variogram( + model=self, + x_data=x_data, + y_data=y_data, + anis=anis, + sill=sill, + init_guess=init_guess, + weights=weights, + method=method, + loss=loss, + max_eval=max_eval, + return_r2=return_r2, + curve_fit_kwargs=curve_fit_kwargs, + **para_select, + ) + + # bounds setting and checks + + def default_arg_bounds(self): + """Provide default boundaries for arguments. + + Given as a dictionary. + """ + res = { + "var": (0.0, np.inf, "co"), + "len_scale": (0.0, np.inf, "co"), + "nugget": (0.0, np.inf, "co"), + "anis": (0.0, np.inf, "oo"), + } + return res + + def set_arg_bounds(self, check_args=True, **kwargs): + r"""Set bounds for the parameters of the model. + + Parameters + ---------- + check_args : bool, optional + Whether to check if the arguments are in their valid bounds. + In case not, a proper default value will be determined. + Default: True + **kwargs + Parameter name as keyword ("var", "len_scale", "nugget", ) + and a list of 2 or 3 values: ``[a, b]`` or ``[a, b, ]`` where + is one of ``"oo"``, ``"cc"``, ``"oc"`` or ``"co"`` + to define if the bounds are open ("o") or closed ("c"). + """ + return set_arg_bounds(self, check_args, **kwargs) + + def check_arg_bounds(self): + """Check arguments to be within their given bounds.""" + return check_arg_bounds(self) + + # bounds properties + + @property + def var_bounds(self): + """:class:`list`: Bounds for the variance. + + Notes + ----- + Is a list of 2 or 3 values: ``[a, b]`` or ``[a, b, ]`` where + is one of ``"oo"``, ``"cc"``, ``"oc"`` or ``"co"`` + to define if the bounds are open ("o") or closed ("c"). + """ + return self._var_bounds + + @var_bounds.setter + def var_bounds(self, bounds): + self.set_arg_bounds(var=bounds) + + @property + def len_scale_bounds(self): + """:class:`list`: Bounds for the length scale. + + Notes + ----- + Is a list of 2 or 3 values: ``[a, b]`` or ``[a, b, ]`` where + is one of ``"oo"``, ``"cc"``, ``"oc"`` or ``"co"`` + to define if the bounds are open ("o") or closed ("c"). + """ + return self._len_scale_bounds + + @len_scale_bounds.setter + def len_scale_bounds(self, bounds): + self.set_arg_bounds(len_scale=bounds) + + @property + def nugget_bounds(self): + """:class:`list`: Bounds for the nugget. + + Notes + ----- + Is a list of 2 or 3 values: ``[a, b]`` or ``[a, b, ]`` where + is one of ``"oo"``, ``"cc"``, ``"oc"`` or ``"co"`` + to define if the bounds are open ("o") or closed ("c"). + """ + return self._nugget_bounds + + @nugget_bounds.setter + def nugget_bounds(self, bounds): + self.set_arg_bounds(nugget=bounds) + + @property + def anis_bounds(self): + """:class:`list`: Bounds for the nugget. + + Notes + ----- + Is a list of 2 or 3 values: ``[a, b]`` or ``[a, b, ]`` where + is one of ``"oo"``, ``"cc"``, ``"oc"`` or ``"co"`` + to define if the bounds are open ("o") or closed ("c"). + """ + return self._anis_bounds + + @anis_bounds.setter + def anis_bounds(self, bounds): + self.set_arg_bounds(anis=bounds) + + @property + def opt_arg_bounds(self): + """:class:`dict`: Bounds for the optional arguments. + + Notes + ----- + Keys are the opt-arg names and values are lists of 2 or 3 values: + ``[a, b]`` or ``[a, b, ]`` where + is one of ``"oo"``, ``"cc"``, ``"oc"`` or ``"co"`` + to define if the bounds are open ("o") or closed ("c"). + """ + return self._opt_arg_bounds + + @property + def arg_bounds(self): + """:class:`dict`: Bounds for all parameters. + + Notes + ----- + Keys are the arg names and values are lists of 2 or 3 values: + ``[a, b]`` or ``[a, b, ]`` where + is one of ``"oo"``, ``"cc"``, ``"oc"`` or ``"co"`` + to define if the bounds are open ("o") or closed ("c"). + """ + res = { + "var": self.var_bounds, + "len_scale": self.len_scale_bounds, + "nugget": self.nugget_bounds, + "anis": self.anis_bounds, + } + res.update(self.opt_arg_bounds) + return res + + @property + def temporal(self): + """:class:`bool`: Whether the model is a metric spatio-temporal one.""" + return self._temporal + + # geographical coordinates related + + @property + def latlon(self): + """:class:`bool`: Whether the model depends on geographical coords.""" + return self._latlon + + @property + def geo_scale(self): + """:class:`float`: Geographic scaling for geographical coords.""" + return self._geo_scale + + @property + def field_dim(self): + """:class:`int`: The (parametric) field dimension of the model (with time).""" + return 2 + int(self.temporal) if self.latlon else self.dim + + @property + def spatial_dim(self): + """:class:`int`: The spatial field dimension of the model (without time).""" + return 2 if self.latlon else self.dim - int(self.temporal) + + # standard parameters + + @property + def dim(self): + """:class:`int`: The dimension of the model.""" + return self._dim + + @dim.setter + def dim(self, dim): + set_dim(self, dim) + + @property + def var(self): + """:class:`float`: The variance of the model.""" + return self._var + + @var.setter + def var(self, var): + self._var = float(var) + self.check_arg_bounds() + + @property + def nugget(self): + """:class:`float`: The nugget of the model.""" + return self._nugget + + @nugget.setter + def nugget(self, nugget): + self._nugget = float(nugget) + self.check_arg_bounds() + + @property + def len_scale(self): + """:class:`float`: The main length scale of the model.""" + return self._len_scale + + @len_scale.setter + def len_scale(self, len_scale): + self._len_scale, anis = set_len_anis( + self.dim, len_scale, self.anis, self.latlon + ) + self._anis = anis + self.check_arg_bounds() + + @property + def rescale(self): + """:class:`float`: Rescale factor for the length scale of the model.""" + return self._rescale + + @rescale.setter + def rescale(self, rescale): + rescale = self.default_rescale() if rescale is None else rescale + self._rescale = abs(float(rescale)) + + @property + def len_rescaled(self): + """:class:`float`: The rescaled main length scale of the model.""" + return self.len_scale / self.rescale + + @property + def anis(self): + """:class:`numpy.ndarray`: The anisotropy factors of the model.""" + return self._anis + + @anis.setter + def anis(self, anis): + self._len_scale, self._anis = set_len_anis( + self.dim, self.len_scale, anis, self.latlon + ) + self.check_arg_bounds() + + @property + def angles(self): + """:class:`numpy.ndarray`: Rotation angles (in rad) of the model.""" + return self._angles + + @angles.setter + def angles(self, angles): + self._angles = set_model_angles( + self.dim, angles, self.latlon, self.temporal + ) + self.check_arg_bounds() + + @property + def integral_scale(self): + """:class:`float`: The main integral scale of the model. + + Raises + ------ + ValueError + If integral scale is not setable. + """ + return self.calc_integral_scale() + + @integral_scale.setter + def integral_scale(self, integral_scale): + int_scale, anis = set_len_anis( + self.dim, integral_scale, self.anis, self.latlon + ) + old_scale = self.integral_scale + self.anis = anis + self.len_scale = self.len_scale * int_scale / old_scale + if not np.isclose(self.integral_scale, int_scale, rtol=1e-3): + raise ValueError( + f"{self.name}: Integral scale could not be set correctly! " + "Please just provide a 'len_scale'!" + ) + + @property + def hankel_kw(self): + """:class:`dict`: :any:`hankel.SymmetricFourierTransform` kwargs.""" + return self._hankel_kw + + @hankel_kw.setter + def hankel_kw(self, hankel_kw): + if self.needs_fourier_transform: + if self._hankel_kw is None or hankel_kw is None: + self._hankel_kw = copy.copy(HANKEL_DEFAULT) + if hankel_kw is not None: + self._hankel_kw.update(hankel_kw) + if self.dim is not None: + self._sft = SFT(ndim=self.dim, **self.hankel_kw) + + @property + def dist_func(self): + """:class:`tuple` of :any:`callable`: pdf, cdf and ppf. + + Spectral distribution info from the model. + """ + pdf = self.spectral_rad_pdf + cdf = None + ppf = None + if self.has_cdf: + cdf = self.spectral_rad_cdf + if self.has_ppf: + ppf = self.spectral_rad_ppf + return pdf, cdf, ppf + + @property + def has_cdf(self): + """:class:`bool`: State if a cdf is defined by the user.""" + return self._has_cdf() + + @property + def has_ppf(self): + """:class:`bool`: State if a ppf is defined by the user.""" + return self._has_ppf() + + @property + def sill(self): + """:class:`float`: The sill of the variogram. + + Notes + ----- + This is calculated by: + * ``sill = variance + nugget`` + """ + return self.var + self.nugget + + @property + def arg(self): + """:class:`list` of :class:`str`: Names of all arguments.""" + return ["var", "len_scale", "nugget", "anis", "angles"] + self.opt_arg + + @property + def arg_list(self): + """:class:`list` of :class:`float`: Values of all arguments.""" + alist = [self.var, self.len_scale, self.nugget, self.anis, self.angles] + for opt in self.opt_arg: + alist.append(getattr(self, opt)) + return alist + + @property + def iso_arg(self): + """:class:`list` of :class:`str`: Names of isotropic arguments.""" + return ["var", "len_scale", "nugget"] + self.opt_arg + + @property + def iso_arg_list(self): + """:class:`list` of :class:`float`: Values of isotropic arguments.""" + alist = [self.var, self.len_scale, self.nugget] + for opt in self.opt_arg: + alist.append(getattr(self, opt)) + return alist + + @property + def opt_arg(self): + """:class:`list` of :class:`str`: Names of the optional arguments.""" + return self._opt_arg + + @property + def len_scale_vec(self): + """:class:`numpy.ndarray`: The length scales in each direction. + + Notes + ----- + This is calculated by: + * ``len_scale_vec[0] = len_scale`` + * ``len_scale_vec[1] = len_scale*anis[0]`` + * ``len_scale_vec[2] = len_scale*anis[1]`` + """ + res = np.zeros(self.dim, dtype=np.double) + res[0] = self.len_scale + for i in range(1, self.dim): + res[i] = self.len_scale * self.anis[i - 1] + return res + + @property + def integral_scale_vec(self): + """:class:`numpy.ndarray`: The integral scales in each direction. + + Notes + ----- + This is calculated by: + * ``integral_scale_vec[0] = integral_scale`` + * ``integral_scale_vec[1] = integral_scale*anis[0]`` + * ``integral_scale_vec[2] = integral_scale*anis[1]`` + """ + res = np.zeros(self.dim, dtype=np.double) + res[0] = self.integral_scale + for i in range(1, self.dim): + res[i] = self.integral_scale * self.anis[i - 1] + return res + + @property + def name(self): + """:class:`str`: The name of the CovModel class.""" + return self.__class__.__name__ + + @property + def do_rotation(self): + """:any:`bool`: State if a rotation is performed.""" + return not np.all(np.isclose(self.angles, 0.0)) + + @property + def is_isotropic(self): + """:any:`bool`: State if a model is isotropic.""" + return np.all(np.isclose(self.anis, 1.0)) + + def __eq__(self, other): + """Compare CovModels.""" + if not isinstance(other, CovModel): + return False + if isinstance(other, SumModel): + return False + return compare(self, other) + + def __setattr__(self, name, value): + """Set an attribute.""" + super().__setattr__(name, value) + # if an optional variogram argument was given, check bounds + if getattr(self, "_init", False) and name in self.opt_arg: + self.check_arg_bounds() + + def __add__(self, other): + """Add two covariance models and return a SumModel.""" + return SumModel(self, other) + + def __radd__(self, other): + """Add two covariance models and return a SumModel.""" + return SumModel(self, other) + + def __repr__(self): + """Return String representation.""" + return model_repr(self) + + +class SumModel(CovModel): + r"""Class for sums of covariance models. + + This class represents sums of covariance models. The nugget of + each contained model will be set to zero and the sum model will + have its own nugget. + The variance of the sum model is the sum of the sub model variances + and the length scale of the sum model is the variance-weighted sum + of the length scales of the sub models. This is motivated by the fact, + that the integral scale of the sum model is equal to the variance-weighted + sum of the integral scales of the sub models. + An empty sum represents a pure Nugget model. + Resetting the total variance or the total length scale will evenly + scale the variances or length scales of the sub models. + Sum models will have a constant rescale factor of one. + + Parameters + ---------- + *models + tuple of :any:`CovModel` instances or subclasses to sum. + All models will get a nugget of zero and the nugget will + be set in the SumModel directly. + Models need to have matching temporal setting, + latlon setting, anis, angles and geo_scale. + The model dimension will be specified by the first given model. + dim : :class:`int`, optional + dimension of the model. + Includes the temporal dimension if temporal is true. + To specify only the spatial dimension in that case, use `spatial_dim`. + Default: ``3`` or dimension of the first given model (if instance). + vars : :class:`list` of :class:`float`, optional + variances of the models. Will reset present variances. + len_scales : :class:`list` of :class:`float`, optional + length scale of the models. Will reset present length scales. + nugget : :class:`float`, optional + nugget of the sum-model. All summed models will have a nugget of zero. + Default: ``0.0`` + anis : :class:`float` or :class:`list`, optional + anisotropy ratios in the transversal directions [e_y, e_z]. + + * e_y = l_y / l_x + * e_z = l_z / l_x + + If only one value is given in 3D, e_y will be set to 1. + This value will be ignored, if multiple len_scales are given. + Default: ``1.0`` or anis of the first given model (if instance). + angles : :class:`float` or :class:`list`, optional + angles of rotation (given in rad): + + * in 2D: given as rotation around z-axis + * in 3D: given by yaw, pitch, and roll (known as Tait–Bryan angles) + + Default: ``0.0`` or angles of the first given model (if instance). + integral_scale : :class:`float` or :class:`list` or :any:`None`, optional + If given, ``len_scale`` will be ignored and recalculated, + so that the integral scale of the model matches the given one. + Default: :any:`None` + latlon : :class:`bool`, optional + Whether the model is describing 2D fields on earths surface described + by latitude and longitude. When using this, the model will internally + use the associated 'Yadrenko' model to represent a valid model. + This means, the spatial distance :math:`r` will be replaced by + :math:`2\sin(\alpha/2)`, where :math:`\alpha` is the great-circle + distance, which is equal to the spatial distance of two points in 3D. + As a consequence, `dim` will be set to `3` and anisotropy will be + disabled. `geo_scale` can be set to e.g. earth's radius, + to have a meaningful `len_scale` parameter. + Default: False or latlon config of the first given model (if instance). + geo_scale : :class:`float`, optional + Geographic unit scaling in case of latlon coordinates to get a + meaningful length scale unit. + By default, len_scale is assumed to be in radians with latlon=True. + Can be set to :any:`KM_SCALE` to have len_scale in km or + :any:`DEGREE_SCALE` to have len_scale in degrees. + Default: :any:`RADIAN_SCALE` or geo_scale of the first given model (if instance). + temporal : :class:`bool`, optional + Create a metric spatio-temporal covariance model. + Setting this to true will increase `dim` and `field_dim` by 1. + `spatial_dim` will be `field_dim - 1`. + The time-dimension is appended, meaning the pos tuple is (x,y,z,...,t). + Default: False or temporal config of the first given model (if instance). + spatial_dim : :class:`int`, optional + spatial dimension of the model. + If given, the model dimension will be determined from this spatial dimension + and the possible temporal dimension if temporal is ture. + Default: None + var : :class:`float`, optional + Total variance of the sum-model. Will evenly scale present variances. + len_scale : :class:`float` or :class:`list`, optional + Total length scale of the sum-model. Will evenly scale present length scales. + **opt_arg + Optional arguments of the sub-models will have and added index of the sub-model. + Also covers ``var_`` and ``length_scale_`` but they should preferably be + set by ``vars`` and ``length_scales``. + """ + + _add_doc = False + + def __init__(self, *models, **kwargs): + self._init = False + self._models = [] + add_nug = 0.0 + to_init = None + imsg = ( + "SumModel: either all models are CovModel instances or subclasses." + ) + for mod in models: + if isinstance(mod, type) and issubclass(mod, SumModel): + if to_init is not None and not to_init: + raise ValueError(imsg) + to_init = True + continue # treat un-init sum-model as nugget model with 0 nugget + if isinstance(mod, SumModel): + if to_init is not None and to_init: + raise ValueError(imsg) + to_init = False + self._models += copy.deepcopy(mod.models) + add_nug += mod.nugget + elif isinstance(mod, CovModel): + if to_init is not None and to_init: + raise ValueError(imsg) + to_init = False + self._models.append(copy.deepcopy(mod)) + elif isinstance(mod, type) and issubclass(mod, CovModel): + if to_init is not None and not to_init: + raise ValueError(imsg) + to_init = True + self._models.append(mod(**default_mod_kwargs(kwargs))) + else: + msg = "SumModel: models need to be instances or subclasses of CovModel." + raise ValueError(msg) + # handle parameters when only Nugget models given + if models and not self.models: + for mod in models: + if not isinstance(mod, type): + kwargs.setdefault("dim", mod.dim) + kwargs.setdefault("latlon", mod.latlon) + kwargs.setdefault("temporal", mod.temporal) + kwargs.setdefault("geo_scale", mod.geo_scale) + kwargs.setdefault("anis", mod.anis) + kwargs.setdefault("angles", mod.angles) + break + # pop entries that can't be re-set + self._latlon = bool( + kwargs.pop( + "latlon", self.models[0].latlon if self.models else False + ) + ) + self._temporal = bool( + kwargs.pop( + "temporal", self.models[0].temporal if self.models else False + ) + ) + self._geo_scale = float( + kwargs.pop( + "geo_scale", + self.models[0].geo_scale if self.models else RADIAN_SCALE, + ) + ) + var_set = kwargs.pop("var", None) + len_set = kwargs.pop("len_scale", None) + # convert nugget + self._nugget = float( + kwargs.pop( + "nugget", + sum((mod.nugget for mod in self.models), 0) + add_nug, + ) + ) + for mod in self.models: + mod._nugget = 0.0 + # prepare dim setting + if "spatial_dim" in kwargs: + spatial_dim = kwargs.pop("spatial_dim") + if spatial_dim is not None: + kwargs["dim"] = spatial_dim + int(self.temporal) + self._dim = None + self._hankel_kw = None + self._sft = None + self.dim = kwargs.get("dim", self.models[0].dim if self.models else 3) + # prepare parameters (they are checked in dim setting) + anis = kwargs.get("anis", self.models[0].anis if self.models else 1) + angles = kwargs.get( + "angles", self.models[0].angles if self.models else 0 + ) + _, self._anis = set_len_anis(self.dim, 1.0, anis, self.latlon) + self._angles = set_model_angles( + self.dim, angles, self.latlon, self.temporal + ) + # prepare parameter boundaries + self._var_bounds = None + self._len_scale_bounds = None + self._nugget_bounds = None + self._anis_bounds = None + self._opt_arg_bounds = {} + bounds = self.default_arg_bounds() + bounds.update(self.default_opt_arg_bounds()) + self.set_arg_bounds(check_args=False, **bounds) + # finalize init + self._prec = 3 + self._init = True + # set remaining args + for arg, val in kwargs.items(): + setattr(self, arg, val) + # reset total variance and length scale last + if var_set is not None: + self.var = var_set + if len_set is not None: + self.len_scale = len_set + # check consistency of sub models + self.check() + + def __iter__(self): + return iter(self.models) + + def __len__(self): + return self.size + + def __contains__(self, item): + return item in self.models + + def __getitem__(self, key): + return self.models[key] + + def check(self): + """Check consistency of contained models.""" + sum_check(self) + + def default_arg_bounds(self): + """Default boundaries for arguments as dict.""" + return sum_default_arg_bounds(self) + + def default_opt_arg_bounds(self): + """Defaults boundaries for optional arguments as dict.""" + return sum_default_opt_arg_bounds(self) + + def set_var_weights(self, weights, skip=None, var=None): + """ + Set variances of contained models by weights. + + The variances of the sub-models act as ratios for the sum-model. + This function enables to keep the total variance of the sum-model + and reset the individual variances of the contained sub-models + by the given weights. + + Parameters + ---------- + weights : iterable + Weights to set. Should have a length of len(models) - len(skip) + skip : iterable, optional + Model indices to skip. Should have compatible length, by default None + var : float, optional + Desired variance, by default current variance + + Raises + ------ + ValueError + If number of weights is not matching. + """ + sum_set_var_weights(self, weights, skip, var) + + def set_len_weights(self, weights, skip=None, len_scale=None): + """ + Set length scales of contained models by weights. + + The variances of the sub-models act as ratios for the sub-model length + scales to determine the total length scale of the sum-model. + This function enables to keep the total length scale of the sum-model as + well as the variances of the sub-models and reset the individual length scales + of the contained sub-models by the given weights. + + Parameters + ---------- + weights : iterable + Weights to set. Should have a length of len(models) - len(skip) + skip : iterable, optional + Model indices to skip. Should have compatible length, by default None + len_scale : float, optional + Desired len_scale, by default current len_scale + + Raises + ------ + ValueError + If number of weights is not matching. + """ + sum_set_len_weights(self, weights, skip, len_scale) + + @property + def models(self): + """:class:`tuple`: The summed models.""" + return self._models + + @property + def size(self): + """:class:`int`: Number of summed models.""" + return len(self._models) + + @property + def rescale(self): + """:class:`float`: SumModel has a constant rescale factor of one.""" + return 1.0 + + @property + def geo_scale(self): + """:class:`float`: Geographic scaling for geographical coords.""" + return self._geo_scale + + @geo_scale.setter + def geo_scale(self, geo_scale): + self._geo_scale = abs(float(geo_scale)) + for mod in self.models: + mod.geo_scale = geo_scale + + @property + def dim(self): + """:class:`int`: The dimension of the model.""" + return self._dim + + @dim.setter + def dim(self, dim): + set_dim(self, dim) + for mod in self.models: + mod.dim = dim + + @property + def var(self): + """:class:`float`: The variance of the model.""" + return sum((var for var in self.vars), 0.0) + + @var.setter + def var(self, var): + for mod, rat in zip(self.models, self.ratios): + mod.var = rat * var + if not self.models and not np.isclose(var, 0): + msg = f"{self.name}: variance needs to be 0." + raise ValueError(msg) + check_arg_in_bounds(self, "var", error=True) + check_arg_in_bounds(self, "len_scale", error=True) + + @property + def vars(self): + """:class:`list`: The variances of the models.""" + return [mod.var for mod in self.models] + + @vars.setter + def vars(self, variances): + if len(variances) != len(self): + msg = "SumModel: number of given variances not matching" + raise ValueError(msg) + for mod, var in zip(self.models, variances): + mod.var = var + check_arg_in_bounds(self, "var", error=True) + check_arg_in_bounds(self, "len_scale", error=True) + + @property + def len_scale(self): + """:class:`float`: The main length scale of the model.""" + return sum( + ( + mod.len_scale * rat + for mod, rat in zip(self.models, self.ratios) + ), + 0.0, + ) + + @len_scale.setter + def len_scale(self, len_scale): + len_scale, anis = set_len_anis( + self.dim, len_scale, self.anis, self.latlon + ) + old_scale = self.len_scale + self.anis = anis + for mod in self.models: + mod.len_scale = mod.len_scale * len_scale / old_scale + if not self.models and not np.isclose(len_scale, 0): + msg = f"{self.name}: length scale needs to be 0." + raise ValueError(msg) + check_arg_in_bounds(self, "len_scale", error=True) + + @property + def len_scales(self): + """:class:`list`: The main length scales of the models.""" + return [mod.len_scale for mod in self.models] + + @len_scales.setter + def len_scales(self, len_scales): + if len(len_scales) != len(self): + msg = "SumModel: number of given length scales not matching" + raise ValueError(msg) + for mod, len_scale in zip(self.models, len_scales): + mod.len_scale = len_scale + check_arg_in_bounds(self, "len_scale", error=True) + + @property + def anis(self): + """:class:`numpy.ndarray`: The anisotropy factors of the model.""" + return self._anis + + @anis.setter + def anis(self, anis): + _, self._anis = set_len_anis(self.dim, 1.0, anis, self.latlon) + for mod in self.models: + mod.anis = anis + check_arg_in_bounds(self, "anis", error=True) + + @property + def angles(self): + """:class:`numpy.ndarray`: Rotation angles (in rad) of the model.""" + return self._angles + + @angles.setter + def angles(self, angles): + self._angles = set_model_angles( + self.dim, angles, self.latlon, self.temporal + ) + for mod in self.models: + mod.angles = angles + + @property + def ratios(self): + """:class:`numpy.ndarray`: Variance ratios of the sub-models.""" + var = self.var + if np.isclose(var, 0) and len(self) > 0: + return np.full(len(self), 1 / len(self)) + return np.array([mod.var / var for mod in self.models]) + + @ratios.setter + def ratios(self, ratios): + if len(ratios) != len(self): + msg = "SumModel.ratios: wrong number of given ratios." + raise ValueError(msg) + if ratios and not np.isclose(np.sum(ratios), 1): + msg = "SumModel.ratios: given ratios do not sum to 1." + raise ValueError(msg) + var = self.var + for mod, rat in zip(self.models, ratios): + mod.var = var * rat + check_arg_in_bounds(self, "var", error=True) + check_arg_in_bounds(self, "len_scale", error=True) + + def calc_integral_scale(self): + return sum( + ( + mod.integral_scale * rat + for mod, rat in zip(self.models, self.ratios) + ), + 0.0, + ) + + @property + def needs_fourier_transform(self): + """Whether the model doesn't implement an analytical spectral density.""" + return False + + def spectral_density(self, k): + return sum( + ( + mod.spectral_density(k) * rat + for mod, rat in zip(self.models, self.ratios) + ), + np.zeros_like(k, dtype=np.double), + ) + + def correlation(self, r): + """SumModel correlation function.""" + return sum( + ( + mod.correlation(r) * rat + for mod, rat in zip(self.models, self.ratios) + ), + np.zeros_like(r, dtype=np.double), + ) + + @property + def opt_arg(self): + """:class:`list` of :class:`str`: Names of the optional arguments.""" + return sum( + [ + [f"{opt}_{i}" for opt in mod.opt_arg] + for i, mod in enumerate(self.models) + ], + [], + ) + + @property + def sub_arg(self): + """:class:`list` of :class:`str`: Names of the sub-arguments for var and len_scale.""" + return [ + f"{o}_{i}" for o in ["var", "len_scale"] for i in range(self.size) + ] + + @property + def sub_arg_bounds(self): + """:class:`dict`: Names of the sub-arguments for var and len_scale.""" + return { + f"{o}_{i}": mod.arg_bounds[o] + for o in ["var", "len_scale"] + for (i, mod) in enumerate(self.models) + } + + @property + def arg_bounds(self): + """:class:`dict`: Bounds for all parameters. + + Notes + ----- + Keys are the arg names and values are lists of 2 or 3 values: + ``[a, b]`` or ``[a, b, ]`` where + is one of ``"oo"``, ``"cc"``, ``"oc"`` or ``"co"`` + to define if the bounds are open ("o") or closed ("c"). + """ + res = { + "var": self.var_bounds, + "len_scale": self.len_scale_bounds, + "nugget": self.nugget_bounds, + "anis": self.anis_bounds, + } + res.update(self.opt_arg_bounds) + res.update(self.sub_arg_bounds) + return res + + def __setattr__(self, name, value): + """Set an attribute.""" + sub_arg = False + if getattr(self, "_init", False): + for i, mod in enumerate(self.models): + if name == f"var_{i}": + mod.var = value + sub_arg = True + if name == f"len_scale_{i}": + mod.len_scale = value + sub_arg = True + if name == f"integral_scale_{i}": + mod.integral_scale = value + sub_arg = True + for opt in mod.opt_arg: + if name == f"{opt}_{i}": + setattr(mod, opt, value) + sub_arg = True + if sub_arg: + break + if sub_arg: + self.check_arg_bounds() + else: + super().__setattr__(name, value) + + def __getattr__(self, name): + """Get an attribute.""" + # __getattr__ is only called when an attribute is not found in the usual places + if name != "_init" and getattr(self, "_init", False): + for i, mod in enumerate(self.models): + if name == f"var_{i}": + return mod.var + if name == f"len_scale_{i}": + return mod.len_scale + if name == f"integral_scale_{i}": + return mod.integral_scale + for opt in mod.opt_arg: + if name == f"{opt}_{i}": + return getattr(mod, opt) + raise AttributeError(f"'{self.name}' object has no attribute '{name}'") + + def __eq__(self, other): + """Compare SumModels.""" + if not isinstance(other, SumModel): + return False + return sum_compare(self, other) + + def __repr__(self): + """Return String representation.""" + return sum_model_repr(self) diff --git a/GSTools/source/src/gstools/covmodel/fit.py b/GSTools/source/src/gstools/covmodel/fit.py new file mode 100644 index 0000000000000000000000000000000000000000..bb882bf04125034b763f855bf163e551ff58d9c0 --- /dev/null +++ b/GSTools/source/src/gstools/covmodel/fit.py @@ -0,0 +1,756 @@ +""" +GStools subpackage providing tools for the covariance-model. + +.. currentmodule:: gstools.covmodel.fit + +The following classes and functions are provided + +.. autosummary:: + fit_variogram +""" + +import numpy as np +from scipy.optimize import curve_fit + +from gstools.covmodel.tools import check_arg_in_bounds, default_arg_from_bounds +from gstools.tools.geometric import great_circle_to_chordal, set_anis + +__all__ = ["fit_variogram"] + + +def fit_variogram( + model, + x_data, + y_data, + anis=True, + sill=None, + init_guess="default", + weights=None, + method="trf", + loss="soft_l1", + max_eval=None, + return_r2=False, + curve_fit_kwargs=None, + **para_select, +): + """ + Fitting a variogram-model to an empirical variogram. + + Parameters + ---------- + model : :any:`CovModel` + Covariance Model to fit. + x_data : :class:`numpy.ndarray` + The bin-centers of the empirical variogram. + y_data : :class:`numpy.ndarray` + The measured variogram + If multiple are given, they are interpreted as the directional + variograms along the main axis of the associated rotated + coordinate system. + Anisotropy ratios will be estimated in that case. + anis : :class:`bool`, optional + In case of a directional variogram, you can control anisotropy + by this argument. Deselect the parameter from fitting, by setting + it "False". + You could also pass a fixed value to be set in the model. + Then the anisotropy ratios won't be altered during fitting. + Default: True + sill : :class:`float` or :class:`bool` or :any:`None`, optional + Here you can provide a fixed sill for the variogram. + It needs to be in a fitting range for the var and nugget bounds. + If variance or nugget are not selected for estimation, + the nugget will be recalculated to fulfill: + + * sill = var + nugget + * if the variance is bigger than the sill, + nugget will bet set to its lower bound + and the variance will be set to the fitting partial sill. + + If variance is deselected, it needs to be less than the sill, + otherwise a ValueError comes up. Same for nugget. + If sill=False, it will be deselected from estimation + and set to the current sill of the model. + Then, the procedure above is applied. + Default: None + init_guess : :class:`str` or :class:`dict`, optional + Initial guess for the estimation. Either: + + * "default": using the default values of the covariance model + ("len_scale" will be mean of given bin centers; + "var" and "nugget" will be mean of given variogram values + (if in given bounds)) + * "current": using the current values of the covariance model + * dict: dictionary with parameter names and given value + (separate "default" can bet set to "default" or "current" for + unspecified values to get same behavior as given above + ("default" by default)) + Example: ``{"len_scale": 10, "default": "current"}`` + + Default: "default" + weights : :class:`str`, :class:`numpy.ndarray`, :class:`callable`optional + Weights applied to each point in the estimation. Either: + + * 'inv': inverse distance ``1 / (x_data + 1)`` + * list: weights given per bin + * callable: function applied to x_data + + If callable, it must take a 1-d ndarray. Then ``weights = f(x_data)``. + Default: None + method : {'trf', 'dogbox'}, optional + Algorithm to perform minimization. + + * 'trf' : Trust Region Reflective algorithm, particularly suitable + for large sparse problems with bounds. Generally robust method. + * 'dogbox' : dogleg algorithm with rectangular trust regions, + typical use case is small problems with bounds. Not recommended + for problems with rank-deficient Jacobian. + + Default: 'trf' + loss : :class:`str` or :class:`callable`, optional + Determines the loss function in scipys curve_fit. + The following keyword values are allowed: + + * 'linear' (default) : ``rho(z) = z``. Gives a standard + least-squares problem. + * 'soft_l1' : ``rho(z) = 2 * ((1 + z)**0.5 - 1)``. The smooth + approximation of l1 (absolute value) loss. Usually a good + choice for robust least squares. + * 'huber' : ``rho(z) = z if z <= 1 else 2*z**0.5 - 1``. Works + similarly to 'soft_l1'. + * 'cauchy' : ``rho(z) = ln(1 + z)``. Severely weakens outliers + influence, but may cause difficulties in optimization process. + * 'arctan' : ``rho(z) = arctan(z)``. Limits a maximum loss on + a single residual, has properties similar to 'cauchy'. + + If callable, it must take a 1-d ndarray ``z=f**2`` and return an + array_like with shape (3, m) where row 0 contains function values, + row 1 contains first derivatives and row 2 contains second + derivatives. Default: 'soft_l1' + max_eval : :class:`int` or :any:`None`, optional + Maximum number of function evaluations before the termination. + If None (default), the value is chosen automatically: 100 * n. + return_r2 : :class:`bool`, optional + Whether to return the r2 score of the estimation. + Default: False + curve_fit_kwargs : :class:`dict`, optional + Other keyword arguments passed to scipys curve_fit. Default: None + **para_select + You can deselect parameters from fitting, by setting + them "False" using their names as keywords. + You could also pass fixed values for each parameter. + Then these values will be applied and the involved parameters wont + be fitted. + By default, all parameters are fitted. + + Returns + ------- + fit_para : :class:`dict` + Dictionary with the fitted parameter values + pcov : :class:`numpy.ndarray` + The estimated covariance of `popt` from + :any:`scipy.optimize.curve_fit`. + To compute one standard deviation errors + on the parameters use ``perr = np.sqrt(np.diag(pcov))``. + r2_score : :class:`float`, optional + r2 score of the curve fitting results. Only if return_r2 is True. + + Notes + ----- + You can set the bounds for each parameter by accessing + :any:`CovModel.set_arg_bounds`. + + The fitted parameters will be instantly set in the model. + """ + # preprocess selected parameters + para, sill, anis, sum_cfg = _pre_para(model, para_select, sill, anis) + # check curve_fit kwargs + curve_fit_kwargs = curve_fit_kwargs or {} + # check method + if method not in ["trf", "dogbox"]: + raise ValueError("fit: method needs to be either 'trf' or 'dogbox'") + # prepare variogram data + # => concatenate directional variograms to have a 1D array for x and y + x_data, y_data, is_dir_vario = _check_vario(model, x_data, y_data) + # only fit anisotropy if a directional variogram was given + anis &= is_dir_vario + sub_fitting = sum_cfg.get("var_size", 0) + sum_cfg.get("len_size", 0) > 0 + if not (any(para.values()) or anis or sub_fitting): + raise ValueError("fit: no parameters selected for fitting.") + # prepare init guess dictionary + init_guess = _pre_init_guess( + model, init_guess, np.mean(x_data), np.mean(y_data) + ) + # set weights + _set_weights(model, weights, x_data, curve_fit_kwargs, is_dir_vario) + # set the lower/upper boundaries for the variogram-parameters + bounds, init_guess_list = _init_curve_fit_para( + model, para, init_guess, sill, anis, sum_cfg + ) + # create the fitting curve + curve_fit_kwargs["f"] = _get_curve( + model, para, sill, anis, is_dir_vario, sum_cfg + ) + # set the remaining kwargs for curve_fit + curve_fit_kwargs["bounds"] = bounds + curve_fit_kwargs["p0"] = init_guess_list + curve_fit_kwargs["xdata"] = x_data + curve_fit_kwargs["ydata"] = y_data + curve_fit_kwargs["loss"] = loss + curve_fit_kwargs["max_nfev"] = max_eval + curve_fit_kwargs["method"] = method + # fit the variogram + popt, pcov = curve_fit(**curve_fit_kwargs) + # convert the results + fit_para = _post_fitting( + model, para, popt, sill, anis, is_dir_vario, sum_cfg + ) + # calculate the r2 score if wanted + if return_r2: + return fit_para, pcov, _r2_score(model, x_data, y_data, is_dir_vario) + return fit_para, pcov + + +def _pre_para(model, para_select, sill, anis): + """Preprocess selected parameters.""" + sum_cfg = {"fix": {}} + is_sum = hasattr(model, "sub_arg") + sub_args = getattr(model, "sub_arg", []) + valid_args = model.iso_arg + sub_args + bnd = model.arg_bounds + # if values given, set them in the model, afterwards all entries are bool + for par, val in para_select.items(): + if par not in valid_args: + raise ValueError(f"fit: unknown parameter in selection: {par}") + # if parameters given with value, set it and deselect from fitting + if not isinstance(val, bool): + # don't set sub-args, var or len_scale in sum model yet + if is_sum and par in sub_args + ["var", "len_scale"]: + sum_cfg["fix"][par] = float(val) + else: + setattr(model, par, float(val)) + para_select[par] = False + # remove those that were set to True + para_select = {k: v for k, v in para_select.items() if not v} + # handling sum models + if is_sum: + _check_sum(model, para_select, sum_cfg, bnd) + # handling the sill + sill = None if (isinstance(sill, bool) and sill) else sill + if sill is not None: + sill = model.sill if isinstance(sill, bool) else float(sill) + _check_sill(model, para_select, sill, bnd, sum_cfg) + # select all parameters to be fitted (if bounds do not indicate fixed parameter) + para = {par: bnd[par][0] < bnd[par][1] for par in valid_args} + # now deselect unwanted parameters + para.update(para_select) + # check if anisotropy should be fitted or set + if not isinstance(anis, bool): + model.anis = anis + anis = False + return para, sill, anis, sum_cfg + + +def _check_sill(model, para_select, sill, bnd, sum_cfg): + """ + This functions checks if the selected values for + variance, nugget and sill are valid. + """ + is_sum = hasattr(model, "sub_arg") + sill_low = bnd["var"][0] + bnd["nugget"][0] + sill_up = bnd["var"][1] + bnd["nugget"][1] + if not sill_low <= sill <= sill_up: + raise ValueError("fit: sill out of bounds.") + if is_sum: + var_fixed = sum_cfg["var_fix"] + else: + var_fixed = "var" in para_select + if var_fixed and "nugget" in para_select: + if not np.isclose(model.var + model.nugget, sill): + msg = "fit: if sill, var and nugget are fixed, var + nugget should match the given sill" + raise ValueError(msg) + elif var_fixed: + if model.var > sill: + raise ValueError( + "fit: if sill is fixed and variance deselected, " + "the set variance should be less than the given sill." + ) + para_select["nugget"] = False + # this also works for a pure nugget model + model.nugget = sill - model.var + elif "nugget" in para_select: + if model.nugget > sill: + raise ValueError( + "fit: if sill is fixed and nugget deselected, " + "the set nugget should be less than the given sill." + ) + para_select["var"] = False + var = sill - model.nugget + if is_sum: + model.set_var_weights( + np.ones_like(sum_cfg["var_fit"]), sum_cfg["var_skip"], var + ) + # if only 1 sub-var was to fit, this is now also fixed + if len(sum_cfg["var_fit"]) == 1: + i = sum_cfg["var_fit"][0] + sum_cfg["fix"].setdefault(f"var_{i}", model.vars[i]) + sum_cfg["var_fit"] = [] + sum_cfg["var_skip"] = list(range(model.size)) + sum_cfg["fix"].setdefault("var", var) + sum_cfg["var_fix"] = True + # number or sub-var parameters + sum_cfg["var_size"] = max( + model.size - len(sum_cfg["var_skip"]) - 1, 0 + ) + else: + # in case of a nugget model, this should raise an error + model.var = var + else: + # deselect the nugget, to recalculate it accordingly + # nugget = sill - var + para_select["nugget"] = False + + +def _check_sum(model, para_select, sum_cfg, bnd): + """Check for consistent parameter selection in case of a SumModel.""" + # check len_scale + if "len_scale" in para_select: + for par in para_select: + if par.startswith("len_scale_"): + msg = ( + "fit: for sum-models you can only fix " + "'len_scale' or the sub-arguments 'len_scale_', not both." + ) + raise ValueError(msg) + sum_cfg["fix"].setdefault("len_scale", model.len_scale) + # use len_scale_ for fitting if len_scale not fixed + # use weights for fitting if len_scale fixed + # either way: len_scale not used for fitting in sum_model + para_select["len_scale"] = False + # check variance + if "var" in para_select: + sum_cfg["fix"].setdefault("var", model.var) + # use var_ for fitting if var not fixed + # use weights for fitting if var fixed + # either way: var not used for fitting in sum_model + para_select["var"] = False + # whether var and len_scale are fixed + var_fix = "var" in sum_cfg["fix"] + len_fix = "len_scale" in sum_cfg["fix"] + size = model.size + # check for fixed bounds + for i in range(size): + for par in [f"var_{i}", f"len_scale_{i}"]: + if not bnd[par][0] < bnd[par][1]: + para_select[par] = False + # check sub arguments (var_ and len_scale_) + var_skip = [] + len_skip = [] + remove = [] + for par in para_select: + if par.startswith("var_"): + var_skip.append(int(par[4:])) + # for fixed var, fit by weights + if var_fix: + remove.append(par) + if par.startswith("len_scale_"): + len_skip.append(int(par[10:])) + for par in remove: + para_select.pop(par) + var_skip.sort() + len_skip.sort() + var_fit = sorted(set(range(size)) - set(var_skip)) + len_fit = sorted(set(range(size)) - set(len_skip)) + # if all sub-vars fixed, total variance is fixed + if not var_fit: + para_select["var"] = False + # set values related to var and len_scale in sum-model + for i in var_skip: + sum_cfg["fix"].setdefault(f"var_{i}", model.vars[i]) + setattr(model, f"var_{i}", sum_cfg["fix"][f"var_{i}"]) + for i in len_skip: + sum_cfg["fix"].setdefault(f"len_scale_{i}", model.len_scales[i]) + setattr(model, f"len_scale_{i}", sum_cfg["fix"][f"len_scale_{i}"]) + if var_fix: + var_min = sum(sum_cfg["fix"][f"var_{i}"] for i in var_skip) + if var_min > sum_cfg["fix"]["var"]: + msg = "fit: fixed sub-variances greater than fixed total variance." + raise ValueError(msg) + model.set_var_weights( + np.ones_like(var_fit), var_skip, sum_cfg["fix"]["var"] + ) + # if all sub_vars except one is fixed and total var as well, all are fixed + if len(var_fit) == 1: + var_fit = [] + var_skip = list(range(size)) + if len_fix: + model.len_scale = sum_cfg["fix"]["len_scale"] + # update config for the sum-model fitting + sum_cfg["var_skip"] = var_skip # sub vars to skip + sum_cfg["len_skip"] = len_skip # sub lens to skip + sum_cfg["var_fit"] = var_fit # sub vars to fit + sum_cfg["len_fit"] = len_fit # sub lens to fit + sum_cfg["var_fix"] = var_fix # total variance fixed + sum_cfg["len_fix"] = len_fix # total len-scale fixed + # number or sub-var parameters + sum_cfg["var_size"] = max(size - len(var_skip) - int(var_fix), 0) + # number or sub-len parameters + sum_cfg["len_size"] = max(size - len(len_skip) - int(len_fix), 0) + + +def _pre_init_guess(model, init_guess, mean_x=1.0, mean_y=1.0): + # init guess should be a dict + if not isinstance(init_guess, dict): + init_guess = {"default": init_guess} + # "default" init guess is the respective default value + default_guess = init_guess.pop("default", "default") + if default_guess not in ["default", "current"]: + raise ValueError(f"fit_variogram: unknown def. guess: {default_guess}") + default = default_guess == "default" + is_sum = hasattr(model, "sub_arg") + sub_args = getattr(model, "sub_arg", []) + valid_args = model.iso_arg + sub_args + ["anis"] + # check invalid names for given init guesses + invalid_para = set(init_guess) - set(valid_args) + if invalid_para: + raise ValueError(f"fit_variogram: unknown init guess: {invalid_para}") + bnd = model.arg_bounds + # default length scale is mean of given bin centers (respecting "rescale") + len_def = mean_x * model.rescale + init_guess.setdefault("len_scale", len_def if default else model.len_scale) + # init guess for variance and nugget is mean of given variogram + for par in ["var", "nugget"]: + init_guess.setdefault(par, mean_y if default else getattr(model, par)) + # anis setting + init_guess.setdefault( + "anis", default_arg_from_bounds(bnd["anis"]) if default else model.anis + ) + # correctly handle given values for anis (need a list of values) + init_guess["anis"] = list(set_anis(model.dim, init_guess["anis"])) + # set optional arguments + for opt in model.opt_arg: + init_guess.setdefault( + opt, + ( + default_arg_from_bounds(bnd[opt]) + if default + else getattr(model, opt) + ), + ) + # SumModel: check for var_ and len_scale_ and set defaults + if is_sum: + for i in range(model.size): + init_guess.setdefault( + f"len_scale_{i}", len_def if default else model.len_scales[i] + ) + init_guess.setdefault( + f"var_{i}", mean_y / model.size if default else model.vars[i] + ) + # convert all init guesses to float (except "anis") + for arg in model.iso_arg + sub_args: + init_guess[arg] = float(init_guess[arg]) + return init_guess + + +def _check_vario(model, x_data, y_data): + # prepare variogram data + x_data = np.asarray(x_data).reshape(-1) + y_data = np.asarray(y_data).reshape(-1) + # if multiple variograms are given, they will be interpreted + # as directional variograms along the main rotated axes of the model + is_dir_vario = False + if model.dim > 1 and x_data.size * model.dim == y_data.size: + is_dir_vario = True + # concatenate multiple variograms + x_data = np.tile(x_data, model.dim) + elif x_data.size != y_data.size: + raise ValueError( + "CovModel.fit_variogram: Wrong number of empirical variograms! " + "Either provide only one variogram to fit an isotropic model, " + "or directional ones for all main axes to fit anisotropy." + ) + if is_dir_vario and model.latlon: + raise ValueError( + "CovModel.fit_variogram: lat-lon models don't support anisotropy." + ) + if model.latlon: + # convert to yadrenko model + x_data = great_circle_to_chordal(x_data, model.geo_scale) + return x_data, y_data, is_dir_vario + + +def _set_weights(model, weights, x_data, curve_fit_kwargs, is_dir_vario): + if weights is not None: + if callable(weights): + weights = 1.0 / weights(x_data) + elif isinstance(weights, str) and weights == "inv": + weights = 1.0 + x_data + else: + if is_dir_vario and weights.size * model.dim == x_data.size: + weights = np.tile(weights, model.dim) + weights = 1.0 / np.asarray(weights).reshape(-1) + curve_fit_kwargs["sigma"] = weights + curve_fit_kwargs["absolute_sigma"] = True + + +def _init_curve_fit_para(model, para, init_guess, sill, anis, sum_cfg): + """Create initial guess and bounds for fitting.""" + is_sum = hasattr(model, "sub_arg") + low_bounds = [] + top_bounds = [] + init_guess_list = [] + bnd = model.arg_bounds + for par in model.iso_arg: + if para[par]: + low_bounds.append(bnd[par][0]) + if par == "var" and sill is not None: # var <= sill in this case + top_bounds.append(sill) + else: + top_bounds.append(bnd[par][1]) + init_guess_list.append( + _init_guess( + bounds=[low_bounds[-1], top_bounds[-1]], + default=init_guess[par], + ) + ) + if is_sum: + if sum_cfg["var_fix"]: + for _ in range(sum_cfg["var_size"]): + low_bounds.append(0.0) + top_bounds.append(1.0) + init_guess_list.append(0.5) + else: + for i in sum_cfg["var_fit"]: + par = f"var_{i}" + low_bounds.append(bnd[par][0]) + top_bounds.append(bnd[par][1]) + init_guess_list.append( + _init_guess( + bounds=[low_bounds[-1], top_bounds[-1]], + default=init_guess[par], + ) + ) + if sum_cfg["len_fix"]: + for _ in range(sum_cfg["len_size"]): + low_bounds.append(0.0) + top_bounds.append(1.0) + init_guess_list.append(0.5) + else: + for i in sum_cfg["len_fit"]: + par = f"len_scale_{i}" + low_bounds.append(bnd[par][0]) + top_bounds.append(bnd[par][1]) + init_guess_list.append( + _init_guess( + bounds=[low_bounds[-1], top_bounds[-1]], + default=init_guess[par], + ) + ) + if anis: + for i in range(model.dim - 1): + low_bounds.append(model.anis_bounds[0]) + top_bounds.append(model.anis_bounds[1]) + init_guess_list.append( + _init_guess( + bounds=[low_bounds[-1], top_bounds[-1]], + default=init_guess["anis"][i], + ) + ) + return (low_bounds, top_bounds), init_guess_list + + +def _init_guess(bounds, default): + """Proper determination of initial guess.""" + if bounds[0] < default < bounds[1]: + return default + return default_arg_from_bounds(bounds) + + +def _get_curve(model, para, sill, anis, is_dir_vario, sum_cfg): + """Create the curve for scipys curve_fit.""" + is_sum = hasattr(model, "sub_arg") + + # we need arg1, otherwise curve_fit throws an error (bug?!) + def curve(x, arg1, *args): + """Adapted Variogram function.""" + args = (arg1,) + args + para_skip = 0 + for par in model.iso_arg: + if para[par]: + setattr(model, par, args[para_skip]) + para_skip += 1 + # set var and len-scale ratios in sum-models + if is_sum: + if sum_cfg["var_size"] > 0: + var_vals = args[para_skip : para_skip + sum_cfg["var_size"]] + para_skip += sum_cfg["var_size"] + if sum_cfg["var_fix"]: + model.set_var_weights( + stick_breaking_uniform(var_vals), + sum_cfg["var_skip"], + sum_cfg["fix"]["var"], + ) + else: + for i, val in zip(sum_cfg["var_fit"], var_vals): + setattr(model, f"var_{i}", val) + if sum_cfg["len_size"] > 0: + len_vals = args[para_skip : para_skip + sum_cfg["len_size"]] + para_skip += sum_cfg["len_size"] + if sum_cfg["len_fix"]: + model.set_len_weights( + stick_breaking_uniform(len_vals), + sum_cfg["len_skip"], + sum_cfg["fix"]["len_scale"], + ) + else: + for i, val in zip(sum_cfg["len_fit"], len_vals): + setattr(model, f"len_scale_{i}", val) + # handle sill + if sill is not None and para["var"]: + nugget_tmp = sill - model.var + # punishment, if resulting nugget out of range for fixed sill + if check_arg_in_bounds(model, "nugget", nugget_tmp) > 0: + return np.full_like(x, np.inf) + # nugget estimation deselected in this case + model.nugget = nugget_tmp + if is_dir_vario: + if anis: + model.anis = args[1 - model.dim :] + xs = x[: x.size // model.dim] + out = np.array([], dtype=np.double) + for i in range(model.dim): + out = np.concatenate((out, model.vario_axis(xs, axis=i))) + return out + return model.variogram(x) + + return curve + + +def _post_fitting(model, para, popt, sill, anis, is_dir_vario, sum_cfg): + """Postprocess fitting results and application to model.""" + is_sum = hasattr(model, "sub_arg") + fit_para = {} + para_skip = 0 + for par in model.iso_arg: + if para[par]: + setattr(model, par, popt[para_skip]) + fit_para[par] = popt[para_skip] + para_skip += 1 + else: + fit_para[par] = getattr(model, par) + # set var and len-scale ratios in sum-models + if is_sum: + if sum_cfg["var_size"] > 0: + var_vals = popt[para_skip : para_skip + sum_cfg["var_size"]] + para_skip += sum_cfg["var_size"] + if sum_cfg["var_fix"]: + model.set_var_weights( + stick_breaking_uniform(var_vals), + sum_cfg["var_skip"], + sum_cfg["fix"]["var"], + ) + else: + for i, val in zip(sum_cfg["var_fit"], var_vals): + setattr(model, f"var_{i}", val) + if sum_cfg["len_size"] > 0: + len_vals = popt[para_skip : para_skip + sum_cfg["len_size"]] + para_skip += sum_cfg["len_size"] + if sum_cfg["len_fix"]: + model.set_len_weights( + stick_breaking_uniform(len_vals), + sum_cfg["len_skip"], + sum_cfg["fix"]["len_scale"], + ) + else: + for i, val in zip(sum_cfg["len_fit"], len_vals): + setattr(model, f"len_scale_{i}", val) + for i in range(model.size): + fit_para[f"var_{i}"] = model.vars[i] + fit_para[f"len_scale_{i}"] = model.len_scales[i] + # handle sill + if sill is not None and para["var"]: + nugget = sill - model.var + fit_para["nugget"] = nugget + model.nugget = nugget + if is_dir_vario: + if anis: + model.anis = popt[1 - model.dim :] + fit_para["anis"] = model.anis + return fit_para + + +def _r2_score(model, x_data, y_data, is_dir_vario): + """Calculate the R2 score.""" + if is_dir_vario: + xs = x_data[: x_data.size // model.dim] + vario = np.array([], dtype=np.double) + for i in range(model.dim): + vario = np.concatenate((vario, model.vario_axis(xs, axis=i))) + else: + vario = model.variogram(x_data) + residuals = y_data - vario + ss_res = np.sum(residuals**2) + ss_tot = np.sum((y_data - np.mean(y_data)) ** 2) + return 1.0 - (ss_res / ss_tot) + + +def logistic_weights(p=0.1, mean=0.7): # pragma: no cover + """ + Return a logistic weights function. + + Parameters + ---------- + p : :class:`float`, optional + Parameter for the growth rate. + Within this percentage of the data range, the function will + be in the upper resp. lower percentile p. The default is 0.1. + mean : :class:`float`, optional + Percentage of the data range, where this function has its + sigmoid's midpoint. The default is 0.7. + + Returns + ------- + callable + Weighting function. + """ + + # define the callable weights function + def func(x_data): + """Callable function for the weights.""" + x_range = np.amax(x_data) - np.amin(x_data) + # logit function for growth rate + growth = np.log(p / (1 - p)) / (p * x_range) + x_mean = mean * x_range + np.amin(x_data) + return 1.0 / (1.0 + np.exp(growth * (x_mean - x_data))) + + return func + + +def stick_breaking_uniform(u): + """ + Generate a single sample (x_1, ..., x_n) uniformly from the (n-1)-simplex. + + This is using Beta transforms of uniform samples. The x_i will sum to 1. + + Parameters + ---------- + u : array-like of shape (n-1,) + Uniform(0,1) random values + + Returns + ------- + x : ndarray of shape (n,) + A random sample in the (n-1)-simplex. + """ + n = len(u) + 1 + x = np.zeros(n) + leftover = 1.0 + for i in range(n - 1): + # 2) Compute the inverse CDF of Beta(1, b) = Beta(1, n-1-i) + fraction = 1.0 - (1.0 - u[i]) ** (1.0 / (n - 1 - i)) + # 3) Break that fraction of the current leftover + x[i] = leftover * fraction + # 4) Subtract that from leftover + leftover -= x[i] + # Last coordinate + x[-1] = leftover + return x diff --git a/GSTools/source/src/gstools/covmodel/models.py b/GSTools/source/src/gstools/covmodel/models.py new file mode 100644 index 0000000000000000000000000000000000000000..3c47b351c5f89ce7d8bc2443564a7ead6b232b47 --- /dev/null +++ b/GSTools/source/src/gstools/covmodel/models.py @@ -0,0 +1,1027 @@ +""" +GStools subpackage providing different covariance models. + +.. currentmodule:: gstools.covmodel.models + +The following classes are provided + +.. autosummary:: + Nugget + Gaussian + Exponential + Matern + Integral + Stable + Rational + Cubic + Linear + Circular + Spherical + HyperSpherical + SuperSpherical + JBessel +""" + +import warnings + +import numpy as np +from scipy import special as sps + +from gstools.covmodel.base import CovModel, SumModel +from gstools.covmodel.tools import AttributeWarning +from gstools.tools.special import exp_int, inc_gamma_low + +__all__ = [ + "Nugget", + "Gaussian", + "Exponential", + "Matern", + "Integral", + "Stable", + "Rational", + "Cubic", + "Linear", + "Circular", + "Spherical", + "HyperSpherical", + "SuperSpherical", + "JBessel", +] + + +class Nugget(SumModel): + r"""Pure nugget model. + + This model has no correlated variability and represents pure noise. + The length scale of the model will be zero. + + Parameters + ---------- + dim : :class:`int`, optional + dimension of the model. + Includes the temporal dimension if temporal is true. + To specify only the spatial dimension in that case, use `spatial_dim`. + Default: ``3`` + nugget : :class:`float`, optional + nugget of the model. Default: ``0.0`` + anis : :class:`float` or :class:`list`, optional + anisotropy ratios in the transversal directions [e_y, e_z]. + + * e_y = l_y / l_x + * e_z = l_z / l_x + + If only one value is given in 3D, e_y will be set to 1. + This value will be ignored, if multiple len_scales are given. + Default: ``1.0`` + angles : :class:`float` or :class:`list`, optional + angles of rotation (given in rad): + + * in 2D: given as rotation around z-axis + * in 3D: given by yaw, pitch, and roll (known as Tait–Bryan angles) + + Default: ``0.0`` + latlon : :class:`bool`, optional + Whether the model is describing 2D fields on earths surface described + by latitude and longitude. When using this, the model will internally + use the associated 'Yadrenko' model to represent a valid model. + This means, the spatial distance :math:`r` will be replaced by + :math:`2\sin(\alpha/2)`, where :math:`\alpha` is the great-circle + distance, which is equal to the spatial distance of two points in 3D. + As a consequence, `dim` will be set to `3` and anisotropy will be + disabled. `geo_scale` can be set to e.g. earth's radius, + to have a meaningful `len_scale` parameter. + Default: False + geo_scale : :class:`float`, optional + Geographic unit scaling in case of latlon coordinates to get a + meaningful length scale unit. + By default, len_scale is assumed to be in radians with latlon=True. + Can be set to :any:`KM_SCALE` to have len_scale in km or + :any:`DEGREE_SCALE` to have len_scale in degrees. + Default: :any:`RADIAN_SCALE` + temporal : :class:`bool`, optional + Create a metric spatio-temporal covariance model. + Setting this to true will increase `dim` and `field_dim` by 1. + `spatial_dim` will be `field_dim - 1`. + The time-dimension is appended, meaning the pos tuple is (x,y,z,...,t). + Default: False + spatial_dim : :class:`int`, optional + spatial dimension of the model. + If given, the model dimension will be determined from this spatial dimension + and the possible temporal dimension if temporal is ture. + Default: None + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + +class Gaussian(CovModel): + r"""The Gaussian covariance model. + + Notes + ----- + This model is given by the following variogram [Webster2007]_: + + .. math:: + \gamma(r)= + \sigma^{2} + \left(1-\exp\left(-\left(s\cdot\frac{r}{\ell}\right)^{2}\right)\right)+n + + Where the standard rescale factor is :math:`s=\frac{\sqrt{\pi}}{2}`. + + References + ---------- + .. [Webster2007] Webster, R. and Oliver, M. A. + "Geostatistics for environmental scientists.", + John Wiley & Sons. (2007) + """ + + def cor(self, h): + """Gaussian normalized correlation function.""" + return np.exp(-(h**2)) + + def default_rescale(self): + """Gaussian rescaling factor to result in integral scale.""" + return np.sqrt(np.pi) / 2.0 + + def spectral_density(self, k): # noqa: D102 + k = np.asarray(k, dtype=np.double) + return (self.len_rescaled / 2.0 / np.sqrt(np.pi)) ** self.dim * np.exp( + -((k * self.len_rescaled / 2.0) ** 2) + ) + + def spectral_rad_cdf(self, r): + """Gaussian radial spectral cdf.""" + r = np.asarray(r, dtype=np.double) + if self.dim == 1: + return sps.erf(r * self.len_rescaled / 2.0) + if self.dim == 2: + return 1.0 - np.exp(-((r * self.len_rescaled / 2.0) ** 2)) + if self.dim == 3: + return sps.erf( + r * self.len_rescaled / 2.0 + ) - r * self.len_rescaled / np.sqrt(np.pi) * np.exp( + -((r * self.len_rescaled / 2.0) ** 2) + ) + return None # pragma: no cover + + def spectral_rad_ppf(self, u): + """Gaussian radial spectral ppf. + + Notes + ----- + Not defined for 3D. + """ + u = np.asarray(u, dtype=np.double) + if self.dim == 1: + return 2.0 / self.len_rescaled * sps.erfinv(u) + if self.dim == 2: + return 2.0 / self.len_rescaled * np.sqrt(-np.log(1.0 - u)) + return None # pragma: no cover + + def _has_cdf(self): + return self.dim in [1, 2, 3] + + def _has_ppf(self): + return self.dim in [1, 2] + + def calc_integral_scale(self): # noqa: D102 + return self.len_rescaled * np.sqrt(np.pi) / 2.0 + + +class Exponential(CovModel): + r"""The Exponential covariance model. + + Notes + ----- + This model is given by the following variogram [Webster2007]_: + + .. math:: + \gamma(r)= + \sigma^{2} + \left(1-\exp\left(-s\cdot\frac{r}{\ell}\right)\right)+n + + Where the standard rescale factor is :math:`s=1`. + + References + ---------- + .. [Webster2007] Webster, R. and Oliver, M. A. + "Geostatistics for environmental scientists.", + John Wiley & Sons. (2007) + """ + + def cor(self, h): + """Exponential normalized correlation function.""" + return np.exp(-h) + + def spectral_density(self, k): # noqa: D102 + k = np.asarray(k, dtype=np.double) + return ( + self.len_rescaled**self.dim + * sps.gamma((self.dim + 1) / 2.0) + / (np.pi * (1.0 + (k * self.len_rescaled) ** 2)) + ** ((self.dim + 1) / 2.0) + ) + + def spectral_rad_cdf(self, r): + """Exponential radial spectral cdf.""" + r = np.asarray(r, dtype=np.double) + if self.dim == 1: + return np.arctan(r * self.len_rescaled) * 2.0 / np.pi + if self.dim == 2: + return 1.0 - 1.0 / np.sqrt(1.0 + (r * self.len_rescaled) ** 2) + if self.dim == 3: + return ( + ( + np.arctan(r * self.len_rescaled) + - r + * self.len_rescaled + / (1.0 + (r * self.len_rescaled) ** 2) + ) + * 2.0 + / np.pi + ) + return None # pragma: no cover + + def spectral_rad_ppf(self, u): + """Exponential radial spectral ppf. + + Notes + ----- + Not defined for 3D. + """ + u = np.asarray(u, dtype=np.double) + if self.dim == 1: + return np.tan(np.pi / 2 * u) / self.len_rescaled + if self.dim == 2: + u_power = np.divide( + 1, + u**2, + out=np.full_like(u, np.inf), + where=np.logical_not(np.isclose(u, 0)), + ) + return np.sqrt(u_power - 1.0) / self.len_rescaled + return None # pragma: no cover + + def _has_cdf(self): + return self.dim in [1, 2, 3] + + def _has_ppf(self): + return self.dim in [1, 2] + + def calc_integral_scale(self): # noqa: D102 + return self.len_rescaled + + +class Stable(CovModel): + r"""The stable covariance model. + + Notes + ----- + This model is given by the following correlation function + [Wackernagel2003]_: + + .. math:: + \rho(r) = + \exp\left(- \left(s\cdot\frac{r}{\ell}\right)^{\alpha}\right) + + Where the standard rescale factor is :math:`s=1`. + :math:`\alpha` is a shape parameter with :math:`\alpha\in(0,2]` + + References + ---------- + .. [Wackernagel2003] Wackernagel, H. "Multivariate geostatistics", + Springer, Berlin, Heidelberg (2003) + + Other Parameters + ---------------- + alpha : :class:`float`, optional + Shape parameter. Standard range: ``(0, 2]`` + Default: ``1.5`` + """ + + def default_opt_arg(self): + """Defaults for the optional arguments. + + * ``{"alpha": 1.5}`` + + Returns + ------- + :class:`dict` + Defaults for optional arguments + """ + return {"alpha": 1.5} + + def default_opt_arg_bounds(self): + """Defaults for boundaries of the optional arguments. + + * ``{"alpha": [0, 2, "oc"]}`` + + Returns + ------- + :class:`dict` + Boundaries for optional arguments + """ + return {"alpha": [0, 2, "oc"]} + + def check_opt_arg(self): + """Check the optional arguments. + + Warns + ----- + alpha + If alpha is < 0.3, the model tends to a nugget model and gets + numerically unstable. + """ + if self.alpha < 0.3: + warnings.warn( + "Stable: parameter 'alpha' is < 0.3, " + "count with unstable results", + AttributeWarning, + ) + + def cor(self, h): + r"""Stable normalized correlation function.""" + return np.exp(-np.power(h, self.alpha)) + + def calc_integral_scale(self): # noqa: D102 + return self.len_rescaled * sps.gamma(1.0 + 1.0 / self.alpha) + + +class Matern(CovModel): + r"""The Matérn covariance model. + + Notes + ----- + This model is given by the following correlation function [Rasmussen2003]_: + + .. math:: + \rho(r) = + \frac{2^{1-\nu}}{\Gamma\left(\nu\right)} \cdot + \left(\sqrt{\nu}\cdot s\cdot\frac{r}{\ell}\right)^{\nu} \cdot + \mathrm{K}_{\nu}\left(\sqrt{\nu}\cdot s\cdot\frac{r}{\ell}\right) + + Where the standard rescale factor is :math:`s=1`. + :math:`\Gamma` is the gamma function and :math:`\mathrm{K}_{\nu}` + is the modified Bessel function of the second kind. + + :math:`\nu` is a shape parameter and should be >= 0.2. + + If :math:`\nu > 20`, a gaussian model is used, since it represents + the limiting case: + + .. math:: + \rho(r) = + \exp\left(-\left(s\cdot\frac{r}{2\ell}\right)^2\right) + + References + ---------- + .. [Rasmussen2003] Rasmussen, C. E., + "Gaussian processes in machine learning." Summer school on + machine learning. Springer, Berlin, Heidelberg, (2003) + + Other Parameters + ---------------- + nu : :class:`float`, optional + Shape parameter. Standard range: ``[0.2, 30]`` + Default: ``1.0`` + """ + + def default_opt_arg(self): + """Defaults for the optional arguments. + + * ``{"nu": 1.0}`` + + Returns + ------- + :class:`dict` + Defaults for optional arguments + """ + return {"nu": 1.0} + + def default_opt_arg_bounds(self): + """Defaults for boundaries of the optional arguments. + + * ``{"nu": [0.2, 30.0, "cc"]}`` + + Returns + ------- + :class:`dict` + Boundaries for optional arguments + """ + return {"nu": [0.2, 30.0, "cc"]} + + def cor(self, h): + """Matérn normalized correlation function.""" + h = np.asarray(np.abs(h), dtype=np.double) + # for nu > 20 we just use the gaussian model + if self.nu > 20.0: + return np.exp(-((h / 2.0) ** 2)) + # calculate by log-transformation to prevent numerical errors + h_gz = h[h > 0.0] + res = np.ones_like(h) + res[h > 0.0] = np.exp( + (1.0 - self.nu) * np.log(2) + - sps.loggamma(self.nu) + + self.nu * np.log(np.sqrt(self.nu) * h_gz) + ) * sps.kv(self.nu, np.sqrt(self.nu) * h_gz) + # if nu >> 1 we get errors for the farfield, there 0 is approached + res[np.logical_not(np.isfinite(res))] = 0.0 + # covariance is positive + res = np.maximum(res, 0.0) + return res + + def spectral_density(self, k): # noqa: D102 + k = np.asarray(k, dtype=np.double) + x = (k * self.len_rescaled) ** 2 + # for nu > 20 we just use an approximation of the gaussian model + if self.nu > 20.0: + return ( + (self.len_rescaled / np.sqrt(np.pi)) ** self.dim + * np.exp(-x) + * (1 + 0.5 * x**2 / self.nu) + * np.sqrt(1 + x / self.nu) ** (-self.dim) + ) + return (self.len_rescaled / np.sqrt(np.pi)) ** self.dim * np.exp( + -(self.nu + self.dim / 2.0) * np.log(1.0 + x / self.nu) + + sps.loggamma(self.nu + self.dim / 2.0) + - sps.loggamma(self.nu) + - self.dim * np.log(np.sqrt(self.nu)) + ) + + def calc_integral_scale(self): # noqa: D102 + return ( + self.len_rescaled + * np.pi + / np.sqrt(self.nu) + / sps.beta(self.nu, 0.5) + ) + + +class Integral(CovModel): + r"""The Exponential Integral covariance model. + + Notes + ----- + This model is given by the following correlation function [Mueller2021]_: + + .. math:: + \rho(r) = + \frac{\nu}{2}\cdot + E_{1+\frac{\nu}{2}}\left( \left( s\cdot\frac{r}{\ell} \right)^2 \right) + + Where the standard rescale factor is :math:`s=1`. + :math:`E_s(x)` is the exponential integral. + + :math:`\nu` is a shape parameter (1 by default). + + For :math:`\nu \to \infty`, a gaussian model is approached, since it represents + the limiting case: + + .. math:: + \rho(r) = + \exp\left(-\left(s\cdot\frac{r}{\ell}\right)^2\right) + + References + ---------- + .. [Mueller2021] Müller, S., Heße, F., Attinger, S., and Zech, A., + "The extended generalized radial flow model and effective + conductivity for truncated power law variograms", + Adv. Water Resour., 156, 104027, (2021) + + Other Parameters + ---------------- + nu : :class:`float`, optional + Shape parameter. Standard range: ``(0.0, 50]`` + Default: ``1.0`` + """ + + def default_opt_arg(self): + """Defaults for the optional arguments. + + * ``{"nu": 1.0}`` + + Returns + ------- + :class:`dict` + Defaults for optional arguments + """ + return {"nu": 1.0} + + def default_opt_arg_bounds(self): + """Defaults for boundaries of the optional arguments. + + * ``{"nu": [0.0, 50.0, "oc"]}`` + + Returns + ------- + :class:`dict` + Boundaries for optional arguments + """ + return {"nu": [0.0, 50.0, "oc"]} + + def cor(self, h): + """Exponential Integral normalized correlation function.""" + h = np.asarray(h, dtype=np.double) + return 0.5 * self.nu * exp_int(1.0 + 0.5 * self.nu, h**2) + + def spectral_density(self, k): # noqa: D102 + k = np.asarray(k, dtype=np.double) + fac = (0.5 * self.len_rescaled / np.sqrt(np.pi)) ** self.dim + lim = fac * self.nu / (self.nu + self.dim) + # for nu > 50 we just use an approximation of the gaussian model + if self.nu > 50.0: + x = (k * self.len_rescaled / 2) ** 2 + return lim * np.exp(-x) * (1 + 2 * x / (self.nu + self.dim + 2)) + # separate calculation at origin + s = (self.nu + self.dim) / 2 + res = np.empty_like(k) + k_gz = np.logical_not(np.isclose(k, 0)) + x = (k[k_gz] * self.len_rescaled / 2) ** 2 + # limit at k=0 (inc_gamma_low(s, x) / x**s -> 1/s for x -> 0) + res[np.logical_not(k_gz)] = lim + res[k_gz] = 0.5 * self.nu * fac / x**s * inc_gamma_low(s, x) + return res + + def calc_integral_scale(self): # noqa: D102 + return ( + self.len_rescaled * self.nu * np.sqrt(np.pi) / (2 * self.nu + 2.0) + ) + + +class Rational(CovModel): + r"""The rational quadratic covariance model. + + Notes + ----- + This model is given by the following correlation function [Rasmussen2003]_: + + .. math:: + \rho(r) = + \left(1 + \frac{1}{\alpha} \cdot + \left(s\cdot\frac{r}{\ell}\right)^2\right)^{-\alpha} + + Where the standard rescale factor is :math:`s=1`. + :math:`\alpha` is a shape parameter and should be > 0.5. + + For :math:`\alpha\to\infty` this model converges to the Gaussian model: + + .. math:: + \rho(r)= + \exp\left(-\left(s\cdot\frac{r}{\ell}\right)^{2}\right) + + References + ---------- + .. [Rasmussen2003] Rasmussen, C. E., + "Gaussian processes in machine learning." Summer school on + machine learning. Springer, Berlin, Heidelberg, (2003) + + Other Parameters + ---------------- + alpha : :class:`float`, optional + Shape parameter. Standard range: ``[0.5, 50]`` + Default: ``1.0`` + """ + + def default_opt_arg(self): + """Defaults for the optional arguments. + + * ``{"alpha": 1.0}`` + + Returns + ------- + :class:`dict` + Defaults for optional arguments + """ + return {"alpha": 1.0} + + def default_opt_arg_bounds(self): + """Defaults for boundaries of the optional arguments. + + * ``{"alpha": [0.5, 50.0]}`` + + Returns + ------- + :class:`dict` + Boundaries for optional arguments + """ + return {"alpha": [0.5, 50.0]} + + def cor(self, h): + """Rational normalized correlation function.""" + return np.power(1 + h**2 / self.alpha, -self.alpha) + + def calc_integral_scale(self): # noqa: D102 + return ( + self.len_rescaled + * np.sqrt(np.pi * self.alpha) + * sps.gamma(self.alpha - 0.5) + / sps.gamma(self.alpha) + / 2.0 + ) + + +class Cubic(CovModel): + r"""The Cubic covariance model. + + A model with reverse curvature near the origin and a finite range of + correlation. + + Notes + ----- + This model is given by the following correlation function [Chiles2009]_: + + .. math:: + \rho(r) = + \begin{cases} + 1- 7 \left(s\cdot\frac{r}{\ell}\right)^{2} + + \frac{35}{4} \left(s\cdot\frac{r}{\ell}\right)^{3} + - \frac{7}{2} \left(s\cdot\frac{r}{\ell}\right)^{5} + + \frac{3}{4} \left(s\cdot\frac{r}{\ell}\right)^{7} + & r<\frac{\ell}{s}\\ + 0 & r\geq\frac{\ell}{s} + \end{cases} + + Where the standard rescale factor is :math:`s=1`. + + References + ---------- + .. [Chiles2009] Chiles, J. P., & Delfiner, P., + "Geostatistics: modeling spatial uncertainty" (Vol. 497), + John Wiley & Sons. (2009) + """ + + def cor(self, h): + """Spherical normalized correlation function.""" + h = np.minimum(np.abs(h, dtype=np.double), 1.0) + return 1.0 - 7 * h**2 + 8.75 * h**3 - 3.5 * h**5 + 0.75 * h**7 + + +class Linear(CovModel): + r"""The bounded linear covariance model. + + This model is derived from the relative intersection area of + two lines in 1D, where the middle points have a distance of :math:`r` + and the line lengths are :math:`\ell`. + + Notes + ----- + This model is given by the following correlation function [Webster2007]_: + + .. math:: + \rho(r) = + \begin{cases} + 1-s\cdot\frac{r}{\ell} & r<\frac{\ell}{s}\\ + 0 & r\geq\frac{\ell}{s} + \end{cases} + + Where the standard rescale factor is :math:`s=1`. + + References + ---------- + .. [Webster2007] Webster, R. and Oliver, M. A. + "Geostatistics for environmental scientists.", + John Wiley & Sons. (2007) + """ + + def cor(self, h): + """Linear normalized correlation function.""" + return np.maximum(1 - np.abs(h, dtype=np.double), 0.0) + + def check_dim(self, dim): + """Linear model is only valid in 1D.""" + return dim < 2 + + +class Circular(CovModel): + r"""The circular covariance model. + + This model is derived as the relative intersection area of + two discs in 2D, where the middle points have a distance of :math:`r` + and the diameters are given by :math:`\ell`. + + Notes + ----- + This model is given by the following correlation function [Webster2007]_: + + .. math:: + \rho(r) = + \begin{cases} + \frac{2}{\pi}\cdot + \left( + \cos^{-1}\left(s\cdot\frac{r}{\ell}\right) - + s\cdot\frac{r}{\ell}\cdot\sqrt{1-\left(s\cdot\frac{r}{\ell}\right)^{2}} + \right) + & r<\frac{\ell}{s}\\ + 0 & r\geq\frac{\ell}{s} + \end{cases} + + Where the standard rescale factor is :math:`s=1`. + + References + ---------- + .. [Webster2007] Webster, R. and Oliver, M. A. + "Geostatistics for environmental scientists.", + John Wiley & Sons. (2007) + """ + + def cor(self, h): + """Circular normalized correlation function.""" + h = np.asarray(np.abs(h), dtype=np.double) + res = np.zeros_like(h) + # arccos is instable around h=1 + h_l1 = h < 1.0 + h_low = h[h_l1] + res[h_l1] = ( + 2 / np.pi * (np.arccos(h_low) - h_low * np.sqrt(1 - h_low**2)) + ) + return res + + def check_dim(self, dim): + """Circular model is only valid in 1D and 2D.""" + return dim < 3 + + +class Spherical(CovModel): + r"""The Spherical covariance model. + + This model is derived from the relative intersection area of + two spheres in 3D, where the middle points have a distance of :math:`r` + and the diameters are given by :math:`\ell`. + + Notes + ----- + This model is given by the following correlation function [Webster2007]_: + + .. math:: + \rho(r) = + \begin{cases} + 1-\frac{3}{2}\cdot s\cdot\frac{r}{\ell} + + \frac{1}{2}\cdot\left(s\cdot\frac{r}{\ell}\right)^{3} + & r<\frac{\ell}{s}\\ + 0 & r\geq\frac{\ell}{s} + \end{cases} + + Where the standard rescale factor is :math:`s=1`. + + References + ---------- + .. [Webster2007] Webster, R. and Oliver, M. A. + "Geostatistics for environmental scientists.", + John Wiley & Sons. (2007) + """ + + def cor(self, h): + """Spherical normalized correlation function.""" + h = np.minimum(np.abs(h, dtype=np.double), 1.0) + return 1.0 - 1.5 * h + 0.5 * h**3 + + def check_dim(self, dim): + """Spherical model is only valid in 1D, 2D and 3D.""" + return dim < 4 + + +class HyperSpherical(CovModel): + r"""The Hyper-Spherical covariance model. + + This model is derived from the relative intersection area of + two d-dimensional hyperspheres, + where the middle points have a distance of :math:`r` + and the diameters are given by :math:`\ell`. + + In 1D this is the Linear model, in 2D the Circular model + and in 3D the Spherical model. + + Notes + ----- + This model is given by the following correlation function [Matern1960]_: + + .. math:: + \rho(r) = + \begin{cases} + 1-s\cdot\frac{r}{\ell}\cdot\frac{ + _{2}F_{1}\left(\frac{1}{2},-\frac{d-1}{2},\frac{3}{2}, + \left(s\cdot\frac{r}{\ell}\right)^{2}\right)} + {_{2}F_{1}\left(\frac{1}{2},-\frac{d-1}{2},\frac{3}{2},1\right)} + & r<\frac{\ell}{s}\\ + 0 & r\geq\frac{\ell}{s} + \end{cases} + + Where the standard rescale factor is :math:`s=1`. + :math:`d` is the dimension. + + References + ---------- + .. [Matern1960] Matern B., "Spatial Variation", + Swedish National Institute for Forestry Research, (1960) + """ + + def cor(self, h): + """Hyper-Spherical normalized correlation function.""" + h = np.asarray(h, dtype=np.double) + res = np.zeros_like(h) + h_l1 = h < 1 + nu = (self.dim - 1.0) / 2.0 + fac = 1.0 / sps.hyp2f1(0.5, -nu, 1.5, 1) + res[h_l1] = 1 - h[h_l1] * fac * sps.hyp2f1(0.5, -nu, 1.5, h[h_l1] ** 2) + return res + + def spectral_density(self, k): # noqa: D102 + k = np.asarray(k, dtype=np.double) + res = np.empty_like(k) + kl = k * self.len_rescaled + kl_gz = np.logical_not(np.isclose(k, 0)) + res[kl_gz] = sps.gamma(self.dim / 2 + 1) / np.sqrt(np.pi) ** self.dim + res[kl_gz] *= sps.jv(self.dim / 2, kl[kl_gz] / 2) ** 2 + res[kl_gz] /= k[kl_gz] ** self.dim + res[np.logical_not(kl_gz)] = ( + (self.len_rescaled / 4) ** self.dim + / sps.gamma(self.dim / 2 + 1) + / np.sqrt(np.pi) ** self.dim + ) + return res + + +class SuperSpherical(CovModel): + r"""The Super-Spherical covariance model. + + This model is derived from the relative intersection area of + two d-dimensional hyperspheres, + where the middle points have a distance of :math:`r` + and the diameters are given by :math:`\ell`. + It is than valid in all lower dimensions. + By default it coincides with the Hyper-Spherical model. + + Notes + ----- + This model is given by the following correlation function [Matern1960]_: + + .. math:: + \rho(r) = + \begin{cases} + 1-s\cdot\frac{r}{\ell}\cdot\frac{ + _{2}F_{1}\left(\frac{1}{2},-\nu,\frac{3}{2}, + \left(s\cdot\frac{r}{\ell}\right)^{2}\right)} + {_{2}F_{1}\left(\frac{1}{2},-\nu,\frac{3}{2},1\right)} + & r<\frac{\ell}{s}\\ + 0 & r\geq\frac{\ell}{s} + \end{cases} + + Where the standard rescale factor is :math:`s=1`. + :math:`\nu\geq\frac{d-1}{2}` is a shape parameter. + + References + ---------- + .. [Matern1960] Matern B., "Spatial Variation", + Swedish National Institute for Forestry Research, (1960) + + Other Parameters + ---------------- + nu : :class:`float`, optional + Shape parameter. Standard range: ``[(dim-1)/2, 50]`` + Default: ``(dim-1)/2`` + """ + + def default_opt_arg(self): + """Defaults for the optional arguments. + + * ``{"nu": (dim-1)/2}`` + + Returns + ------- + :class:`dict` + Defaults for optional arguments + """ + return {"nu": (self.dim - 1) / 2} + + def default_opt_arg_bounds(self): + """Defaults for boundaries of the optional arguments. + + * ``{"nu": [(dim-1)/2, 50.0]}`` + + Returns + ------- + :class:`dict` + Boundaries for optional arguments + """ + return {"nu": [(self.dim - 1) / 2, 50.0]} + + def cor(self, h): + """Super-Spherical normalized correlation function.""" + h = np.asarray(h, dtype=np.double) + res = np.zeros_like(h) + h_l1 = h < 1 + fac = 1.0 / sps.hyp2f1(0.5, -self.nu, 1.5, 1.0) + res[h_l1] = 1.0 - h[h_l1] * fac * sps.hyp2f1( + 0.5, -self.nu, 1.5, h[h_l1] ** 2 + ) + return res + + +class JBessel(CovModel): + r"""The J-Bessel hole model. + + This covariance model is a valid hole model, meaning it has areas + of negative correlation but a valid spectral density. + + Notes + ----- + This model is given by the following correlation function [Chiles2009]_: + + .. math:: + \rho(r) = + \Gamma(\nu+1) \cdot + \frac{\mathrm{J}_{\nu}\left(s\cdot\frac{r}{\ell}\right)} + {\left(s\cdot\frac{r}{2\ell}\right)^{\nu}} + + Where the standard rescale factor is :math:`s=1`. + :math:`\Gamma` is the gamma function and :math:`\mathrm{J}_{\nu}` + is the Bessel functions of the first kind. + :math:`\nu\geq\frac{d}{2}-1` is a shape parameter, + which defaults to :math:`\nu=\frac{d}{2}`, + since the spectrum of the model gets instable for + :math:`\nu\to\frac{d}{2}-1`. + + For :math:`\nu=\frac{1}{2}` (valid in d=1,2,3) + we get the so-called 'Wave' model: + + .. math:: + \rho(r) = + \frac{\sin\left(s\cdot\frac{r}{\ell}\right)}{s\cdot\frac{r}{\ell}} + + References + ---------- + .. [Chiles2009] Chiles, J. P., & Delfiner, P., + "Geostatistics: modeling spatial uncertainty" (Vol. 497), + John Wiley & Sons. (2009) + + Other Parameters + ---------------- + nu : :class:`float`, optional + Shape parameter. Standard range: ``[dim/2 - 1, 50]`` + Default: ``dim/2`` + """ + + def default_opt_arg(self): + """Defaults for the optional arguments. + + * ``{"nu": dim/2}`` + + Returns + ------- + :class:`dict` + Defaults for optional arguments + """ + return {"nu": self.dim / 2} + + def default_opt_arg_bounds(self): + """Defaults for boundaries of the optional arguments. + + * ``{"nu": [dim/2 - 1, 50.0]}`` + + Returns + ------- + :class:`dict` + Boundaries for optional arguments + """ + return {"nu": [self.dim / 2 - 1, 50.0]} + + def check_opt_arg(self): + """Check the optional arguments. + + Warns + ----- + nu + If nu is close to dim/2 - 1, the model tends to get unstable. + """ + if abs(self.nu - self.dim / 2 + 1) < 0.01: + warnings.warn( + "JBessel: parameter 'nu' is close to d/2-1, " + "count with unstable results", + AttributeWarning, + ) + + def cor(self, h): + """J-Bessel correlation.""" + h = np.asarray(h, dtype=np.double) + h_gz = np.logical_not(np.isclose(h, 0)) + hh = h[h_gz] + res = np.ones_like(h) + nu = self.nu + res[h_gz] = sps.gamma(nu + 1) * sps.jv(nu, hh) / (hh / 2.0) ** nu + return res + + def spectral_density(self, k): # noqa: D102 + k = np.asarray(k, dtype=np.double) + k_ll = k < 1.0 / self.len_rescaled + kk = k[k_ll] + res = np.zeros_like(k) + # the model is degenerated for nu=d/2-1, so we tweak the spectral pdf + # and cut of the divisor at nu-(d/2-1)=0.01 (gamma(0.01) about 100) + res[k_ll] = ( + (self.len_rescaled / np.sqrt(np.pi)) ** self.dim + * sps.gamma(self.nu + 1.0) + / np.minimum(sps.gamma(self.nu - self.dim / 2 + 1), 100.0) + * (1.0 - (kk * self.len_rescaled) ** 2) ** (self.nu - self.dim / 2) + ) + return res diff --git a/GSTools/source/src/gstools/covmodel/plot.py b/GSTools/source/src/gstools/covmodel/plot.py new file mode 100644 index 0000000000000000000000000000000000000000..ce35f9bf897e0db0e406b7365d5f8ecf046c2c58 --- /dev/null +++ b/GSTools/source/src/gstools/covmodel/plot.py @@ -0,0 +1,332 @@ +""" +GStools subpackage providing plotting routines for the covariance models. + +.. currentmodule:: gstools.covmodel.plot + +The following classes and functions are provided + +.. autosummary:: + :toctree: + + plot_variogram + plot_covariance + plot_correlation + plot_vario_yadrenko + plot_cov_yadrenko + plot_cor_yadrenko + plot_vario_axis + plot_cov_axis + plot_cor_axis + plot_vario_spatial + plot_cov_spatial + plot_cor_spatial + plot_spectrum + plot_spectral_density + plot_spectral_rad_pdf +""" + +import numpy as np + +from gstools.tools.geometric import generate_grid +from gstools.tools.misc import get_fig_ax + +__all__ = [ + "plot_variogram", + "plot_covariance", + "plot_correlation", + "plot_vario_yadrenko", + "plot_cov_yadrenko", + "plot_cor_yadrenko", + "plot_vario_axis", + "plot_cov_axis", + "plot_cor_axis", + "plot_vario_spatial", + "plot_cov_spatial", + "plot_cor_spatial", + "plot_spectrum", + "plot_spectral_density", + "plot_spectral_rad_pdf", +] + + +# plotting routines ####################################################### + + +def _plot_spatial(dim, pos, field, fig, ax, temporal, **kwargs): + from gstools.field.plot import plot_1d, plot_nd + + if dim == 1: + return plot_1d(pos, field, fig, ax, temporal, **kwargs) + return plot_nd( + pos, field, "structured", fig, ax, temporal=temporal, **kwargs + ) + + +def plot_vario_spatial( + model, x_min=0.0, x_max=None, fig=None, ax=None, **kwargs +): # pragma: no cover + """Plot spatial variogram of a given CovModel.""" + if x_max is None: + if np.isclose(model.len_scale, 0): + x_max = 1.0 + else: + x_max = 3 * model.len_scale + x_s = np.linspace(-x_max, x_max) + x_min + pos = [x_s] * model.dim + shp = tuple(len(p) for p in pos) + fld = model.vario_spatial(generate_grid(pos)).reshape(shp) + return _plot_spatial( + model.dim, pos, fld, fig, ax, model.temporal, **kwargs + ) + + +def plot_cov_spatial( + model, x_min=0.0, x_max=None, fig=None, ax=None, **kwargs +): # pragma: no cover + """Plot spatial covariance of a given CovModel.""" + if x_max is None: + if np.isclose(model.len_scale, 0): + x_max = 1.0 + else: + x_max = 3 * model.len_scale + x_s = np.linspace(-x_max, x_max) + x_min + pos = [x_s] * model.dim + shp = tuple(len(p) for p in pos) + fld = model.cov_spatial(generate_grid(pos)).reshape(shp) + return _plot_spatial( + model.dim, pos, fld, fig, ax, model.temporal, **kwargs + ) + + +def plot_cor_spatial( + model, x_min=0.0, x_max=None, fig=None, ax=None, **kwargs +): # pragma: no cover + """Plot spatial correlation of a given CovModel.""" + if x_max is None: + if np.isclose(model.len_scale, 0): + x_max = 1.0 + else: + x_max = 3 * model.len_scale + x_s = np.linspace(-x_max, x_max) + x_min + pos = [x_s] * model.dim + shp = tuple(len(p) for p in pos) + fld = model.cor_spatial(generate_grid(pos)).reshape(shp) + return _plot_spatial( + model.dim, pos, fld, fig, ax, model.temporal, **kwargs + ) + + +def plot_variogram( + model, x_min=0.0, x_max=None, fig=None, ax=None, **kwargs +): # pragma: no cover + """Plot variogram of a given CovModel.""" + fig, ax = get_fig_ax(fig, ax) + if x_max is None: + if np.isclose(model.len_scale, 0): + x_max = 1.0 + else: + x_max = 3 * model.len_scale + x_s = np.linspace(x_min, x_max) + kwargs.setdefault("label", f"{model.name} variogram") + ax.plot(x_s, model.variogram(x_s), **kwargs) + ax.legend() + fig.show() + return ax + + +def plot_covariance( + model, x_min=0.0, x_max=None, fig=None, ax=None, **kwargs +): # pragma: no cover + """Plot covariance of a given CovModel.""" + fig, ax = get_fig_ax(fig, ax) + if x_max is None: + if np.isclose(model.len_scale, 0): + x_max = 1.0 + else: + x_max = 3 * model.len_scale + x_s = np.linspace(x_min, x_max) + kwargs.setdefault("label", f"{model.name} covariance") + ax.plot(x_s, model.covariance(x_s), **kwargs) + ax.legend() + fig.show() + return ax + + +def plot_correlation( + model, x_min=0.0, x_max=None, fig=None, ax=None, **kwargs +): # pragma: no cover + """Plot correlation function of a given CovModel.""" + fig, ax = get_fig_ax(fig, ax) + if x_max is None: + if np.isclose(model.len_scale, 0): + x_max = 1.0 + else: + x_max = 3 * model.len_scale + x_s = np.linspace(x_min, x_max) + kwargs.setdefault("label", f"{model.name} correlation") + ax.plot(x_s, model.correlation(x_s), **kwargs) + ax.legend() + fig.show() + return ax + + +def plot_vario_yadrenko( + model, x_min=0.0, x_max=None, fig=None, ax=None, **kwargs +): # pragma: no cover + """Plot Yadrenko variogram of a given CovModel.""" + fig, ax = get_fig_ax(fig, ax) + if x_max is None: + if np.isclose(model.len_scale, 0): + x_max = 1.0 + else: + x_max = min(3 * model.len_scale, model.geo_scale * np.pi) + x_s = np.linspace(x_min, x_max) + kwargs.setdefault("label", f"{model.name} Yadrenko variogram") + ax.plot(x_s, model.vario_yadrenko(x_s), **kwargs) + ax.legend() + fig.show() + return ax + + +def plot_cov_yadrenko( + model, x_min=0.0, x_max=None, fig=None, ax=None, **kwargs +): # pragma: no cover + """Plot Yadrenko covariance of a given CovModel.""" + fig, ax = get_fig_ax(fig, ax) + if x_max is None: + if np.isclose(model.len_scale, 0): + x_max = 1.0 + else: + x_max = min(3 * model.len_scale, model.geo_scale * np.pi) + x_s = np.linspace(x_min, x_max) + kwargs.setdefault("label", f"{model.name} Yadrenko covariance") + ax.plot(x_s, model.cov_yadrenko(x_s), **kwargs) + ax.legend() + fig.show() + return ax + + +def plot_cor_yadrenko( + model, x_min=0.0, x_max=None, fig=None, ax=None, **kwargs +): # pragma: no cover + """Plot Yadrenko correlation function of a given CovModel.""" + fig, ax = get_fig_ax(fig, ax) + if x_max is None: + if np.isclose(model.len_scale, 0): + x_max = 1.0 + else: + x_max = min(3 * model.len_scale, model.geo_scale * np.pi) + x_s = np.linspace(x_min, x_max) + kwargs.setdefault("label", f"{model.name} Yadrenko correlation") + ax.plot(x_s, model.cor_yadrenko(x_s), **kwargs) + ax.legend() + fig.show() + return ax + + +def plot_vario_axis( + model, axis=0, x_min=0.0, x_max=None, fig=None, ax=None, **kwargs +): # pragma: no cover + """Plot variogram of a given CovModel.""" + fig, ax = get_fig_ax(fig, ax) + if x_max is None: + if np.isclose(model.len_scale, 0): + x_max = 1.0 + else: + x_max = 3 * model.len_scale + x_s = np.linspace(x_min, x_max) + kwargs.setdefault("label", f"{model.name} variogram on axis {axis}") + ax.plot(x_s, model.vario_axis(x_s, axis), **kwargs) + ax.legend() + fig.show() + return ax + + +def plot_cov_axis( + model, axis=0, x_min=0.0, x_max=None, fig=None, ax=None, **kwargs +): # pragma: no cover + """Plot variogram of a given CovModel.""" + fig, ax = get_fig_ax(fig, ax) + if x_max is None: + if np.isclose(model.len_scale, 0): + x_max = 1.0 + else: + x_max = 3 * model.len_scale + x_s = np.linspace(x_min, x_max) + kwargs.setdefault("label", f"{model.name} covariance on axis {axis}") + ax.plot(x_s, model.cov_axis(x_s, axis), **kwargs) + ax.legend() + fig.show() + return ax + + +def plot_cor_axis( + model, axis=0, x_min=0.0, x_max=None, fig=None, ax=None, **kwargs +): # pragma: no cover + """Plot variogram of a given CovModel.""" + fig, ax = get_fig_ax(fig, ax) + if x_max is None: + if np.isclose(model.len_scale, 0): + x_max = 1.0 + else: + x_max = 3 * model.len_scale + x_s = np.linspace(x_min, x_max) + kwargs.setdefault("label", f"{model.name} correlation on axis {axis}") + ax.plot(x_s, model.cor_axis(x_s, axis), **kwargs) + ax.legend() + fig.show() + return ax + + +def plot_spectrum( + model, x_min=0.0, x_max=None, fig=None, ax=None, **kwargs +): # pragma: no cover + """Plot spectrum of a given CovModel.""" + fig, ax = get_fig_ax(fig, ax) + if x_max is None: + if np.isclose(model.len_scale, 0): + x_max = 1.0 + else: + x_max = 3 / model.len_scale + x_s = np.linspace(x_min, x_max) + kwargs.setdefault("label", f"{model.name} {model.dim}D spectrum") + ax.plot(x_s, model.spectrum(x_s), **kwargs) + ax.legend() + fig.show() + return ax + + +def plot_spectral_density( + model, x_min=0.0, x_max=None, fig=None, ax=None, **kwargs +): # pragma: no cover + """Plot spectral density of a given CovModel.""" + fig, ax = get_fig_ax(fig, ax) + if x_max is None: + if np.isclose(model.len_scale, 0): + x_max = 1.0 + else: + x_max = 3 / model.len_scale + x_s = np.linspace(x_min, x_max) + kwargs.setdefault("label", f"{model.name} {model.dim}D spectral-density") + ax.plot(x_s, model.spectral_density(x_s), **kwargs) + ax.legend() + fig.show() + return ax + + +def plot_spectral_rad_pdf( + model, x_min=0.0, x_max=None, fig=None, ax=None, **kwargs +): # pragma: no cover + """Plot radial spectral pdf of a given CovModel.""" + fig, ax = get_fig_ax(fig, ax) + if x_max is None: + if np.isclose(model.len_scale, 0): + x_max = 1.0 + else: + x_max = 3 / model.len_scale + x_s = np.linspace(x_min, x_max) + kwargs.setdefault("label", f"{model.name} {model.dim}D spectral-rad-pdf") + ax.plot(x_s, model.spectral_rad_pdf(x_s), **kwargs) + ax.legend() + fig.show() + return ax diff --git a/GSTools/source/src/gstools/covmodel/sum_tools.py b/GSTools/source/src/gstools/covmodel/sum_tools.py new file mode 100644 index 0000000000000000000000000000000000000000..37bdb98e8fe185dcfe7257ac32d350a13d40321f --- /dev/null +++ b/GSTools/source/src/gstools/covmodel/sum_tools.py @@ -0,0 +1,265 @@ +""" +GStools subpackage providing tools for sum-models. + +.. currentmodule:: gstools.covmodel.sum_tools + +The following classes and functions are provided + +.. autosummary:: + ARG_DEF + default_mod_kwargs + sum_check + sum_compare + sum_default_arg_bounds + sum_default_opt_arg_bounds + sum_set_norm_var_ratios + sum_set_norm_len_ratios + sum_model_repr +""" + +import numpy as np + +from gstools.tools import RADIAN_SCALE +from gstools.tools.misc import list_format + +__all__ = [ + "ARG_DEF", + "default_mod_kwargs", + "sum_check", + "sum_compare", + "sum_default_arg_bounds", + "sum_default_opt_arg_bounds", + "sum_set_var_weights", + "sum_set_len_weights", + "sum_model_repr", +] + + +ARG_DEF = { + "dim": 3, + "latlon": False, + "temporal": False, + "geo_scale": RADIAN_SCALE, + "spatial_dim": None, + "hankel_kw": None, +} +"""dict: default model arguments""" + + +def default_mod_kwargs(kwargs): + """Generate default model keyword arguments.""" + mod_kw = {} + for arg, default in ARG_DEF.items(): + mod_kw[arg] = kwargs.get(arg, default) + return mod_kw + + +def sum_check(summod): + """Check consistency of contained models.""" + # prevent dim error in anis and angles + if any(mod.dim != summod.dim for mod in summod): + msg = "SumModel: models need to have same dimension." + raise ValueError(msg) + if any(mod.latlon != summod.latlon for mod in summod): + msg = "SumModel: models need to have same latlon config." + raise ValueError(msg) + if any(mod.temporal != summod.temporal for mod in summod): + msg = "SumModel: models need to have same temporal config." + raise ValueError(msg) + if not all(np.isclose(mod.nugget, 0) for mod in summod): + msg = "SumModel: models need to have 0 nugget." + raise ValueError(msg) + if not np.allclose([mod.geo_scale for mod in summod], summod.geo_scale): + msg = "SumModel: models need to have same geo_scale." + raise ValueError(msg) + if not all(np.allclose(mod.anis, summod.anis) for mod in summod): + msg = "SumModel: models need to have same anisotropy ratios." + raise ValueError(msg) + if not all(np.allclose(mod.angles, summod.angles) for mod in summod): + msg = "SumModel: models need to have same rotation angles." + raise ValueError(msg) + + +def sum_default_arg_bounds(summod): + """Default boundaries for arguments as dict.""" + var_bnds = [mod.var_bounds for mod in summod.models] + len_bnds = [mod.len_scale_bounds for mod in summod.models] + var_lo = sum((bnd[0] for bnd in var_bnds), start=0.0) + var_hi = sum((bnd[1] for bnd in var_bnds), start=0.0) + len_lo = min((bnd[0] for bnd in len_bnds), default=0.0) + len_hi = max((bnd[1] for bnd in len_bnds), default=0.0) + res = { + "var": (var_lo, var_hi), + "len_scale": (len_lo, len_hi), + "nugget": (0.0, np.inf, "co"), + "anis": (0.0, np.inf, "oo"), + } + return res + + +def sum_default_opt_arg_bounds(summod): + """Defaults boundaries for optional arguments as dict.""" + bounds = {} + for i, mod in enumerate(summod.models): + bounds.update( + {f"{opt}_{i}": bnd for opt, bnd in mod.opt_arg_bounds.items()} + ) + return bounds + + +def sum_set_var_weights(summod, weights, skip=None, var=None): + """ + Set variances of contained models by weights. + + Parameters + ---------- + weights : iterable + Weights to set. Should have a length of len(models) - len(skip) + skip : iterable, optional + Model indices to skip. Should have compatible length, by default None + var : float, optional + Desired variance, by default current variance + + Raises + ------ + ValueError + If number of weights is not matching. + """ + skip = set() if skip is None else set(skip) + if len(summod) != len(weights) + len(skip): + msg = "SumModel.set_var_weights: number of ratios not matching." + raise ValueError(msg) + ids = range(len(summod)) + if fail := set(skip) - set(ids): + msg = ( + f"SumModel.set_var_weights: ids given by 'skip' not valid: {fail}" + ) + raise ValueError(msg) + var = summod.var if var is None else float(var) + var_sum = sum(summod.models[i].var for i in skip) + var_diff = var - var_sum + if var_diff < 0: + msg = ( + "SumModel.set_var_weights: summed variances selected " + "with 'skip' already too big to keep total variance." + ) + raise ValueError(msg) + weights_sum = sum(weights) + var_list = summod.vars + j = 0 + for i in ids: + if i in skip: + continue + var_list[i] = var_diff * weights[j] / weights_sum + j += 1 + summod.vars = var_list + + +def sum_set_len_weights(summod, weights, skip=None, len_scale=None): + """ + Set length scales of contained models by weights. + + Parameters + ---------- + weights : iterable + Weights to set. Should have a length of len(models) - len(skip) + skip : iterable, optional + Model indices to skip. Should have compatible length, by default None + len_scale : float, optional + Desired len_scale, by default current len_scale + + Raises + ------ + ValueError + If number of weights is not matching. + """ + skip = set() if skip is None else set(skip) + if len(summod) != len(weights) + len(skip): + msg = "SumModel.set_len_weights: number of weights not matching." + raise ValueError(msg) + ids = range(len(summod)) + if fail := set(skip) - set(ids): + msg = ( + f"SumModel.set_len_weights: ids given by 'skip' not valid: {fail}" + ) + raise ValueError(msg) + len_scale = summod.len_scale if len_scale is None else float(len_scale) + # also skip models with no variance (not contributing to total len scale) + j = 0 + wei = [] + for i in ids: + if i in skip: + continue + if np.isclose(summod.ratios[i], 0): + skip.add(i) + else: + wei.append(weights[j]) + j += 1 + weights = wei + len_sum = sum(summod[i].len_scale * summod.ratios[i] for i in skip) + len_diff = len_scale - len_sum + if len_diff < 0: + msg = ( + "SumModel.set_len_weights: summed length scales " + "selected with 'skip' already too big to keep total length scale." + ) + raise ValueError(msg) + weights_sum = sum(weights) + len_scales = summod.len_scales + j = 0 + for i in ids: + if i in skip: + continue + len_scales[i] = len_diff * weights[j] / weights_sum / summod.ratios[j] + j += 1 + summod.len_scales = len_scales + + +def sum_compare(this, that): + """ + Compare SumModels. + + Parameters + ---------- + this / that : :any:`SumModel` + The sum models to compare. + """ + if len(this) != len(that): + return False + if not np.isclose(this.nugget, that.nugget): + return False + return all(mod1 == mod2 for (mod1, mod2) in zip(this, that)) + + +def sum_model_repr(summod): # pragma: no cover + """ + Generate the sum-model string representation. + + Parameters + ---------- + model : :any:`SumModel` + The sum-model in use. + """ + m, p = summod, summod._prec + ani_str, ang_str, o_str, r_str, p_str = "", "", "", "", "" + m_str = ", ".join([mod.name for mod in m.models]) + t_str = ", temporal=True" if m.temporal else "" + d_str = f"latlon={m.latlon}" if m.latlon else f"dim={m.spatial_dim}" + if len(m) > 0: + m_str += ", " + p_str += f", vars={list_format(m.vars, p)}" + p_str += f", len_scales={list_format(m.len_scales, p)}" + p_str += "" if np.isclose(m.nugget, 0) else f", nugget={m.nugget:.{p}}" + for opt in m.opt_arg: + o_str += f", {opt}={getattr(m, opt):.{p}}" + if m.latlon: + if not m.is_isotropic and m.temporal: + ani_str = f", anis={m.anis[-1]:.{p}}" + if not np.isclose(m.geo_scale, 1): + r_str = f", geo_scale={m.geo_scale:.{p}}" + else: + if not m.is_isotropic: + ani_str = f", anis={list_format(m.anis, p)}" + if m.do_rotation: + ang_str = f", angles={list_format(m.angles, p)}" + return f"{m.name}({m_str}{d_str}{t_str}{p_str}{ani_str}{ang_str}{r_str}{o_str})" diff --git a/GSTools/source/src/gstools/covmodel/tools.py b/GSTools/source/src/gstools/covmodel/tools.py new file mode 100644 index 0000000000000000000000000000000000000000..1d26d0628aac758b2a88fbe8c098282fe8e83fb3 --- /dev/null +++ b/GSTools/source/src/gstools/covmodel/tools.py @@ -0,0 +1,630 @@ +""" +GStools subpackage providing tools for the covariance-model. + +.. currentmodule:: gstools.covmodel.tools + +The following classes and functions are provided + +.. autosummary:: + AttributeWarning + rad_fac + set_opt_args + set_len_anis + check_bounds + check_arg_in_bounds + default_arg_from_bounds + spectral_rad_pdf + percentile_scale + set_arg_bounds + check_arg_bounds + set_dim + compare + model_repr +""" + +import warnings + +import numpy as np +from hankel import SymmetricFourierTransform as SFT +from scipy import special as sps +from scipy.optimize import root + +from gstools.tools.geometric import no_of_angles, set_angles, set_anis +from gstools.tools.misc import list_format + +__all__ = [ + "AttributeWarning", + "rad_fac", + "set_opt_args", + "set_len_anis", + "set_model_angles", + "check_bounds", + "check_arg_in_bounds", + "default_arg_from_bounds", + "spectral_rad_pdf", + "percentile_scale", + "set_arg_bounds", + "check_arg_bounds", + "set_dim", + "compare", + "model_repr", +] + + +class AttributeWarning(UserWarning): + """Attribute warning for CovModel class.""" + + +def _init_subclass(cls): + """Initialize gstools covariance model.""" + + def variogram(self, r): + """Isotropic variogram of the model.""" + return self.var - self.covariance(r) + self.nugget + + def covariance(self, r): + """Covariance of the model.""" + return self.var * self.correlation(r) + + def correlation(self, r): + """Correlation function of the model.""" + return 1.0 - (self.variogram(r) - self.nugget) / self.var + + def correlation_from_cor(self, r): + """Correlation function of the model.""" + r = np.asarray(np.abs(r), dtype=np.double) + return self.cor(r / self.len_rescaled) + + def cor_from_correlation(self, h): + """Correlation taking a non-dimensional range.""" + h = np.asarray(np.abs(h), dtype=np.double) + return self.correlation(h * self.len_rescaled) + + abstract = True + if hasattr(cls, "cor"): + if not hasattr(cls, "correlation"): + cls.correlation = correlation_from_cor + abstract = False + else: + cls.cor = cor_from_correlation + if not hasattr(cls, "variogram"): + cls.variogram = variogram + else: + abstract = False + if not hasattr(cls, "covariance"): + cls.covariance = covariance + else: + abstract = False + if not hasattr(cls, "correlation"): + cls.correlation = correlation + else: + abstract = False + if abstract: + raise TypeError( + f"Can't instantiate class '{cls.__name__}', " + "without providing at least one of the methods " + "'cor', 'variogram', 'covariance' or 'correlation'." + ) + + +# Helping functions ########################################################### + + +def rad_fac(dim, r): + """Volume element of the n-dimensional spherical coordinates. + + Given as a factor for integration of a radial-symmetric function. + + Parameters + ---------- + dim : :class:`int` + spatial dimension + r : :class:`numpy.ndarray` + Given radii. + """ + if dim == 1: + fac = 2.0 + elif dim == 2: + fac = 2 * np.pi * r + elif dim == 3: + fac = 4 * np.pi * r**2 + else: # pragma: no cover + fac = ( + dim + * r ** (dim - 1) + * np.sqrt(np.pi) ** dim + / sps.gamma(dim / 2 + 1) + ) + return fac + + +def set_opt_args(model, opt_arg): + """ + Set optional arguments in the model class. + + Parameters + ---------- + model : :any:`CovModel` + The covariance model in use. + opt_arg : :class:`dict` + Dictionary with optional arguments. + + Raises + ------ + ValueError + When an optional argument has an already taken name. + """ + model._opt_arg = [] + # look up the defaults for the optional arguments (defined by the user) + default = model.default_opt_arg() + for opt_name in opt_arg: + if opt_name not in default: + warnings.warn( + f"The given optional argument '{opt_name}' " + "is unknown or has at least no defined standard value. " + "Or you made a Typo... hehe.", + AttributeWarning, + ) + # add the default values if not specified + for def_arg in default: + if def_arg not in opt_arg: + opt_arg[def_arg] = default[def_arg] + # save names of the optional arguments (sort them by name) + model._opt_arg = sorted(opt_arg) + # add the optional arguments as attributes to the class + for opt_name in opt_arg: + if opt_name in dir(model): # "dir" also respects properties + raise ValueError( + f"parameter '{opt_name}' has a 'bad' name, " + "since it is already present in " + "the class. It could not be added to the model." + ) + # Magic happens here + setattr(model, opt_name, float(opt_arg[opt_name])) + + +def set_len_anis(dim, len_scale, anis, latlon=False): + """Set the length scale and anisotropy factors for the given dimension. + + Parameters + ---------- + dim : :class:`int` + spatial dimension + len_scale : :class:`float` or :class:`list` + the length scale of the SRF in x direction or in x- (y-, ...) direction + anis : :class:`float` or :class:`list` + the anisotropy of length scales along the transversal axes + latlon : :class:`bool`, optional + Whether the model is describing 2D fields on earths surface described + by latitude and longitude. In this case there is no spatial anisotropy. + Default: False + + Returns + ------- + len_scale : :class:`float` + the main length scale of the SRF in x direction + anis : :class:`list`, optional + the anisotropy of length scales along the transversal axes + + Notes + ----- + If ``len_scale`` is given by at least two values, + ``anis`` will be recalculated. + + If ``len_scale`` is given as list with to few values, the latter value will + be used for the remaining dimensions. (e.g. [l_1, l_2] in 3D is equal to + [l_1, l_2, l_2]) + + If to few ``anis`` values are given, the first dimensions will be filled + up with 1. (eg. anis=[e] in 3D is equal to anis=[1, e]) + """ + ls_tmp = np.array(len_scale, dtype=np.double) + ls_tmp = np.atleast_1d(ls_tmp)[:dim] + # use just one length scale (x-direction) + out_len_scale = ls_tmp[0] + # set the anisotropies in y- and z-direction according to the input + if len(ls_tmp) == 1: + out_anis = set_anis(dim, anis) + else: + # fill up length-scales with the latter len_scale, such that len()==dim + if len(ls_tmp) < dim: + ls_tmp = np.pad(ls_tmp, (0, dim - len(ls_tmp)), "edge") + # if multiple length-scales are given, calculate the anisotropies + out_anis = np.zeros(dim - 1, dtype=np.double) + for i in range(1, dim): + out_anis[i - 1] = ls_tmp[i] / ls_tmp[0] + # sanity check + for ani in out_anis: + if not ani > 0.0: + raise ValueError( + f"anisotropy-ratios needs to be > 0, got: {out_anis}" + ) + # no spatial anisotropy for latlon + if latlon: + out_anis[:2] = 1.0 + return out_len_scale, out_anis + + +def set_model_angles(dim, angles, latlon=False, temporal=False): + """Set the model angles for the given dimension. + + Parameters + ---------- + dim : :class:`int` + spatial dimension + angles : :class:`float` or :class:`list` + the angles of the SRF + latlon : :class:`bool`, optional + Whether the model is describing 2D fields on earths surface described + by latitude and longitude. + Default: False + temporal : :class:`bool`, optional + Whether a time-dimension is appended. + Default: False + + Returns + ------- + angles : :class:`float` + the angles fitting to the dimension + + Notes + ----- + If too few angles are given, they are filled up with `0`. + """ + if latlon: + return np.array(no_of_angles(dim) * [0], dtype=np.double) + out_angles = set_angles(dim, angles) + if temporal: + # no rotation between spatial dimensions and temporal dimension + out_angles[no_of_angles(dim - 1) :] = 0.0 + return out_angles + + +def check_bounds(bounds): + """ + Check if given bounds are valid. + + Parameters + ---------- + bounds : list + bound can contain 2 to 3 values: + 1. lower bound + float + 2. upper bound + float + 3. Interval type (optional) + * "oo" : open - open + * "oc" : open - close + * "co" : close - open + * "cc" : close - close (default) + """ + typ = bounds[2] if len(bounds) == 3 else "cc" + if len(bounds) not in (2, 3): + return False + if (typ == "cc" and bounds[1] < bounds[0]) or ( + typ != "cc" and bounds[1] <= bounds[0] + ): + return False + if len(bounds) == 3 and bounds[2] not in ("oo", "oc", "co", "cc"): + return False + return True + + +def check_arg_in_bounds(model, arg, val=None, error=False): + """Check if given argument value is in bounds of the given model.""" + if arg not in model.arg_bounds: + raise ValueError(f"check bounds: unknown argument: {arg}") + bnd = list(model.arg_bounds[arg]) + val = getattr(model, arg) if val is None else val + val = np.asarray(val) + error_case = 0 + if len(bnd) == 2: + bnd = list(bnd) + bnd.append("cc") # use closed intervals by default + if bnd[2][0] == "c": + if np.any(val < bnd[0]): + error_case = 1 + else: + if np.any(val <= bnd[0]): + error_case = 2 + if bnd[2][1] == "c": + if np.any(val > bnd[1]): + error_case = 3 + else: + if np.any(val >= bnd[1]): + error_case = 4 + if error: + if error_case == 1: + raise ValueError(f"{arg} needs to be >= {bnd[0]}, got: {val}") + if error_case == 2: + raise ValueError(f"{arg} needs to be > {bnd[0]}, got: {val}") + if error_case == 3: + raise ValueError(f"{arg} needs to be <= {bnd[1]}, got: {val}") + if error_case == 4: + raise ValueError(f"{arg} needs to be < {bnd[1]}, got: {val}") + return error_case + + +def default_arg_from_bounds(bounds): + """ + Determine a default value from given bounds. + + Parameters + ---------- + bounds : list + bounds for the value. + + Returns + ------- + float + Default value in the given bounds. + """ + if bounds[0] > -np.inf and bounds[1] < np.inf: + return (bounds[0] + bounds[1]) / 2.0 + if bounds[0] > -np.inf: + return bounds[0] + 1.0 + if bounds[1] < np.inf: + return bounds[1] - 1.0 + return 0.0 # pragma: no cover + + +# outsourced routines + + +def spectral_rad_pdf(model, r): + """ + Spectral radians PDF of a model. + + Parameters + ---------- + model : :any:`CovModel` + The covariance model in use. + r : :class:`numpy.ndarray` + Given radii. + + Returns + ------- + :class:`numpy.ndarray` + PDF values. + + """ + r = np.asarray(np.abs(r), dtype=np.double) + if model.dim > 1: + r_gz = np.logical_not(np.isclose(r, 0)) + # to prevent numerical errors, we just calculate where r>0 + res = np.zeros_like(r, dtype=np.double) + res[r_gz] = rad_fac(model.dim, r[r_gz]) * np.abs( + model.spectral_density(r[r_gz]) + ) + else: + res = rad_fac(model.dim, r) * np.abs(model.spectral_density(r)) + # prevent numerical errors in hankel for small r values (set 0) + res[np.logical_not(np.isfinite(res))] = 0.0 + # prevent numerical errors in hankel for big r (set non-negative) + res = np.maximum(res, 0.0) + return res + + +def percentile_scale(model, per=0.9): + """ + Calculate the percentile scale of the isotrope model. + + This is the distance, where the given percentile of the variance + is reached by the variogram + + + Parameters + ---------- + model : :any:`CovModel` + The covariance model in use. + per : float, optional + Percentile to use. The default is 0.9. + + Raises + ------ + ValueError + When percentile is not in (0, 1). + + Returns + ------- + float + Percentile scale. + + """ + # check the given percentile + if not 0.0 < per < 1.0: + raise ValueError(f"percentile needs to be within (0, 1), got: {per}") + + # define a curve, that has its root at the wanted point + def curve(x): + return 1.0 - model.correlation(x) - per + + # take 'per * len_rescaled' as initial guess + return root(curve, per * model.len_rescaled)["x"][0] + + +def set_arg_bounds(model, check_args=True, **kwargs): + r"""Set bounds for the parameters of the model. + + Parameters + ---------- + model : :any:`CovModel` + The covariance model in use. + check_args : bool, optional + Whether to check if the arguments are in their valid bounds. + In case not, a proper default value will be determined. + Default: True + **kwargs + Parameter name as keyword ("var", "len_scale", "nugget", ) + and a list of 2 or 3 values as value: + + * ``[a, b]`` or + * ``[a, b, ]`` + + is one of ``"oo"``, ``"cc"``, ``"oc"`` or ``"co"`` + to define if the bounds are open ("o") or closed ("c"). + """ + for arg, bounds in kwargs.items(): + if not check_bounds(bounds): + msg = f"Given bounds for '{arg}' are not valid, got: {bounds}" + raise ValueError(msg) + if arg in getattr(model, "sub_arg", []): + # var_ and len_scale_ + name, i = _split_sub(arg) + setattr(model[i], f"_{name}_bounds", bounds) + elif arg in model.opt_arg: + model._opt_arg_bounds[arg] = bounds + elif arg in model.arg_bounds: + setattr(model, f"_{arg}_bounds", bounds) + else: + raise ValueError(f"set_arg_bounds: unknown argument '{arg}'") + if check_args and check_arg_in_bounds(model, arg) > 0: + def_arg = default_arg_from_bounds(bounds) + if arg == "anis": + setattr(model, arg, [def_arg] * (model.dim - 1)) + else: + setattr(model, arg, def_arg) + + +def _split_sub(name): + if name.startswith("var_"): + return "var", int(name[4:]) + if name.startswith("len_scale_"): + return "len_scale", int(name[10:]) + msg = f"Unknown sub variable: {name}" + raise ValueError(msg) + + +def check_arg_bounds(model): + """ + Check arguments to be within their given bounds. + + Parameters + ---------- + model : :any:`CovModel` + The covariance model in use. + + Raises + ------ + ValueError + When an argument is not in its valid bounds. + """ + # check var, len_scale, nugget and optional-arguments + for arg in model.arg_bounds: + check_arg_in_bounds(model, arg, error=True) + + +def set_dim(model, dim): + """ + Set the dimension in the given model. + + Parameters + ---------- + model : :any:`CovModel` + The covariance model in use. + dim : :class:`int` + dimension of the model. + + Raises + ------ + ValueError + When dimension is < 1. + """ + # check if a fixed dimension should be used + if model.fix_dim() is not None and model.fix_dim() != dim: + warnings.warn( + f"{model.name}: using fixed dimension {model.fix_dim()}", + AttributeWarning, + ) + dim = model.fix_dim() + if model.latlon and dim != (3 + int(model.temporal)): + raise ValueError( + f"{model.name}: using fixed dimension {model.fix_dim()}, " + f"which is not compatible with a latlon model (with temporal={model.temporal})." + ) + # force dim=3 (or 4 when temporal=True) for latlon models + dim = (3 + int(model.temporal)) if model.latlon else dim + # set the dimension + if dim < 1: + raise ValueError("Only dimensions of d >= 1 are supported.") + if not model.check_dim(dim): + warnings.warn( + f"Dimension {dim} is not appropriate for this model.", + AttributeWarning, + ) + model._dim = int(dim) + # create fourier transform just once (recreate for dim change) + if model.needs_fourier_transform: + model._sft = SFT(ndim=model.dim, **model.hankel_kw) + # recalculate dimension related parameters (if model initialized) + if model._init: + model.len_scale, model.anis = set_len_anis( + model.dim, model.len_scale, model.anis + ) + model.angles = set_model_angles( + model.dim, model.angles, model.latlon, model.temporal + ) + model.check_arg_bounds() + + +def compare(this, that): + """ + Compare CovModels. + + Parameters + ---------- + this / that : :any:`CovModel` + The covariance models to compare. + """ + # prevent attribute error in opt_arg if the are not equal + if set(this.opt_arg) != set(that.opt_arg): + return False + # prevent dim error in anis and angles + if this.dim != that.dim: + return False + equal = True + equal &= this.name == that.name + equal &= np.isclose(this.var, that.var) + equal &= np.isclose(this.nugget, that.nugget) + equal &= np.isclose(this.len_scale, that.len_scale) + equal &= np.all(np.isclose(this.anis, that.anis)) + equal &= np.all(np.isclose(this.angles, that.angles)) + equal &= np.isclose(this.rescale, that.rescale) + equal &= np.isclose(this.geo_scale, that.geo_scale) + equal &= this.latlon == that.latlon + equal &= this.temporal == that.temporal + for opt in this.opt_arg: + equal &= np.isclose(getattr(this, opt), getattr(that, opt)) + return equal + + +def model_repr(model): # pragma: no cover + """ + Generate the model string representation. + + Parameters + ---------- + model : :any:`CovModel` + The covariance model in use. + """ + m, p = model, model._prec + ani_str, ang_str, o_str, r_str = "", "", "", "" + t_str = ", temporal=True" if m.temporal else "" + d_str = f"latlon={m.latlon}" if m.latlon else f"dim={m.spatial_dim}" + p_str = f", var={m.var:.{p}}, len_scale={m.len_scale:.{p}}" + p_str += "" if np.isclose(m.nugget, 0) else f", nugget={m.nugget:.{p}}" + if not np.isclose(m.rescale, m.default_rescale()): + o_str += f", rescale={m.rescale:.{p}}" + for opt in m.opt_arg: + o_str += f", {opt}={getattr(m, opt):.{p}}" + if m.latlon: + if not m.is_isotropic and m.temporal: + ani_str = f", anis={m.anis[-1]:.{p}}" + if not np.isclose(m.geo_scale, 1): + r_str = f", geo_scale={m.geo_scale:.{p}}" + else: + if not m.is_isotropic: + ani_str = f", anis={list_format(m.anis, p)}" + if m.do_rotation: + ang_str = f", angles={list_format(m.angles, p)}" + return f"{m.name}({d_str}{t_str}{p_str}{ani_str}{ang_str}{r_str}{o_str})" diff --git a/GSTools/source/src/gstools/covmodel/tpl_models.py b/GSTools/source/src/gstools/covmodel/tpl_models.py new file mode 100644 index 0000000000000000000000000000000000000000..46472d1b77eb052bab52d4aa43b982cca64b462a --- /dev/null +++ b/GSTools/source/src/gstools/covmodel/tpl_models.py @@ -0,0 +1,575 @@ +""" +GStools subpackage providing truncated power law covariance models. + +.. currentmodule:: gstools.covmodel.tpl_models + +The following classes and functions are provided + +.. autosummary:: + TPLGaussian + TPLExponential + TPLStable + TPLSimple +""" + +import warnings + +import numpy as np + +from gstools.covmodel.base import CovModel +from gstools.covmodel.tools import AttributeWarning +from gstools.tools.special import ( + tpl_exp_spec_dens, + tpl_gau_spec_dens, + tplstable_cor, +) + +__all__ = ["TPLGaussian", "TPLExponential", "TPLStable", "TPLSimple"] + + +class TPLCovModel(CovModel): + """Truncated-Power-Law Covariance Model base class for super-position.""" + + @property + def intensity(self): + """:class:`float`: Intensity of variation.""" + return self.var / self.intensity_scaling + + @property + def intensity_scaling(self): + """:class:`float`: Scaling of Intensity to result in variance.""" + return ( + self.len_up_rescaled ** (2 * self.hurst) + - self.len_low_rescaled ** (2 * self.hurst) + ) / (2 * self.hurst) + + @property + def len_up(self): + """:class:`float`: Upper length scale truncation of the model. + + * ``len_up = len_low + len_scale`` + """ + return self.len_low + self.len_scale + + @property + def len_up_rescaled(self): + """:class:`float`: Upper length scale truncation rescaled. + + * ``len_up_rescaled = (len_low + len_scale) / rescale`` + """ + return self.len_up / self.rescale + + @property + def len_low_rescaled(self): + """:class:`float`: Lower length scale truncation rescaled. + + * ``len_low_rescaled = len_low / rescale`` + """ + return self.len_low / self.rescale + + def cor(self, h): + """TPL - normalized correlation function.""" + + def correlation(self, r): + """TPL - correlation function.""" + + +# Truncated power law ######################################################### + + +class TPLGaussian(TPLCovModel): + r"""Truncated-Power-Law with Gaussian modes. + + Notes + ----- + The truncated power law is given by a superposition of scale-dependent + variograms [Federico1997]_: + + .. math:: + \gamma_{\ell_{\mathrm{low}},\ell_{\mathrm{up}}}(r) = + \intop_{\ell_{\mathrm{low}}}^{\ell_{\mathrm{up}}} + \gamma(r,\lambda) \frac{\rm d \lambda}{\lambda} + + with `Gaussian` modes on each scale: + + .. math:: + \gamma(r,\lambda) &= + \sigma^2(\lambda)\cdot\left(1- + \exp\left[- \left(\frac{r}{\lambda}\right)^{2}\right] + \right)\\ + \sigma^2(\lambda) &= C\cdot\lambda^{2H} + + This results in: + + .. math:: + \gamma_{\ell_{\mathrm{low}},\ell_{\mathrm{up}}}(r) &= + \sigma^2_{\ell_{\mathrm{low}},\ell_{\mathrm{up}}}\cdot\left(1- + H \cdot + \frac{\ell_{\mathrm{up}}^{2H} \cdot + E_{1+H} + \left[\left(\frac{r}{\ell_{\mathrm{up}}}\right)^{2}\right] + - \ell_{\mathrm{low}}^{2H} \cdot + E_{1+H} + \left[\left(\frac{r}{\ell_{\mathrm{low}}}\right)^{2}\right]} + {\ell_{\mathrm{up}}^{2H}-\ell_{\mathrm{low}}^{2H}} + \right) \\ + \sigma^2_{\ell_{\mathrm{low}},\ell_{\mathrm{up}}} &= + \frac{C\cdot\left(\ell_{\mathrm{up}}^{2H} + -\ell_{\mathrm{low}}^{2H}\right)}{2H} + + The "length scale" of this model is equivalent by the integration range: + + .. math:: + \ell = \ell_{\mathrm{up}} -\ell_{\mathrm{low}} + + If you want to define an upper scale truncation, you should set ``len_low`` + and ``len_scale`` accordingly. + + The following Parameters occur: + + * :math:`C>0` : + scaling factor from the Power-Law (intensity of variation) + This parameter will be calculated internally by the given variance. + You can access C by ``model.intensity`` + * :math:`00` : + scaling factor from the Power-Law (intensity of variation) + This parameter will be calculated internally by the given variance. + You can access C by ``model.intensity`` + * :math:`00` : + scaling factor from the Power-Law (intensity of variation) + This parameter will be calculated internally by the given variance. + You can access C by ``model.intensity`` + * :math:`0 1 and value.size != dim: # vector mean + raise ValueError(f"Mean/Trend: Wrong size ({value})") + return value if value.size > 1 else value.item() + + +class Field: + """A base class for random fields, kriging fields, etc. + + Parameters + ---------- + model : :any:`CovModel`, optional + Covariance Model related to the field. + value_type : :class:`str`, optional + Value type of the field. Either "scalar" or "vector". + The default is "scalar". + mean : :any:`None` or :class:`float` or :any:`callable`, optional + Mean of the field if wanted. Could also be a callable. + The default is None. + normalizer : :any:`None` or :any:`Normalizer`, optional + Normalizer to be applied to the field. + The default is None. + trend : :any:`None` or :class:`float` or :any:`callable`, optional + Trend of the denormalized fields. If no normalizer is applied, + this behaves equal to 'mean'. + The default is None. + dim : :any:`None` or :class:`int`, optional + Dimension of the field if no model is given. + """ + + valid_value_types = ["scalar", "vector"] + """:class:`list` of :class:`str`: valid field value types.""" + + default_field_names = ["field"] + """:class:`list`: Default field names.""" + + def __init__( + self, + model=None, + value_type="scalar", + mean=None, + normalizer=None, + trend=None, + dim=None, + ): + # initialize attributes + self._mesh_type = "unstructured" # default + self._pos = None + self._field_shape = None + self._field_names = [] + self._model = None + self._value_type = None + self._mean = None + self._normalizer = None + self._trend = None + self._dim = dim if dim is None else int(dim) + # set properties + self.model = model + self.value_type = value_type + self.mean = mean + self.normalizer = normalizer + self.trend = trend + + def __len__(self): + return len(self.field_names) + + def __contains__(self, item): + return item in self.field_names + + def __getitem__(self, key): + if key in self.field_names: + return getattr(self, key) + if isinstance(key, int): + return self[self.field_names[key]] + if isinstance(key, slice): + return [self[f] for f in self.field_names[key]] + if isinstance(key, Iterable) and not isinstance(key, str): + return [self[f] for f in key] + raise KeyError(f"{self.name}: requested field '{key}' not present") + + def __delitem__(self, key): + names = [] + if key in self.field_names: + names = [key] + elif isinstance(key, int): + names = [self.field_names[key]] + elif isinstance(key, slice): + names = self.field_names[key] + elif isinstance(key, Iterable) and not isinstance(key, str): + for k in key: + k = self.field_names[k] if isinstance(key, int) else k + names.append(k) + else: + raise KeyError(f"{self.name}: requested field '{key}' not present") + for name in names: + if name not in self.field_names: + raise KeyError( + f"{self.name}: requested field '{name}' not present" + ) + delattr(self, name) + del self._field_names[self._field_names.index(name)] + + def __call__( + self, + pos=None, + field=None, + mesh_type="unstructured", + post_process=True, + store=True, + ): + """Generate the field. + + Parameters + ---------- + pos : :class:`list`, optional + the position tuple, containing main direction and transversal + directions + field : :class:`numpy.ndarray` or :any:`None`, optional + the field values. Will be all zeros if :any:`None` is given. + mesh_type : :class:`str`, optional + 'structured' / 'unstructured'. Default: 'unstructured' + post_process : :class:`bool`, optional + Whether to apply mean, normalizer and trend to the field. + Default: `True` + store : :class:`str` or :class:`bool`, optional + Whether to store field (True/False) with default name + or with specified name. + The default is :any:`True` for default name "field". + + Returns + ------- + field : :class:`numpy.ndarray` + the field values. + """ + name, save = self.get_store_config(store) + pos, shape = self.pre_pos(pos, mesh_type) + if field is None: + field = np.zeros(shape, dtype=np.double) + else: + field = np.asarray(field, dtype=np.double).reshape(shape) + return self.post_field(field, name, post_process, save) + + def structured(self, *args, **kwargs): + """Generate a field on a structured mesh. + + See :any:`__call__` + """ + if self.pos is None: + self.mesh_type = "structured" + if not (args or "pos" in kwargs) and self.mesh_type == "unstructured": + raise ValueError("Field.structured: can't reuse present 'pos'") + call = partial(self.__call__, mesh_type="structured") + return call(*args, **kwargs) + + def unstructured(self, *args, **kwargs): + """Generate a field on an unstructured mesh. + + See :any:`__call__` + """ + if self.pos is None: + self.mesh_type = "unstructured" + if not (args or "pos" in kwargs) and self.mesh_type != "unstructured": + raise ValueError("Field.unstructured: can't reuse present 'pos'") + call = partial(self.__call__, mesh_type="unstructured") + return call(*args, **kwargs) + + def mesh( + self, mesh, points="centroids", direction="all", name="field", **kwargs + ): + """Generate a field on a given meshio, ogs5py or PyVista mesh. + + Parameters + ---------- + mesh : meshio.Mesh or ogs5py.MSH or PyVista mesh + The given mesh + points : :class:`str`, optional + The points to evaluate the field at. + Either the "centroids" of the mesh cells + (calculated as mean of the cell vertices) or the "points" + of the given mesh. + Default: "centroids" + direction : :class:`str` or :class:`list`, optional + Here you can state which direction should be chosen for + lower dimension. For example, if you got a 2D mesh in xz direction, + you have to pass "xz". By default, all directions are used. + One can also pass a list of indices. + Default: "all" + name : :class:`str` or :class:`list` of :class:`str`, optional + Name(s) to store the field(s) in the given mesh as point_data or + cell_data. If to few names are given, digits will be appended. + Default: "field" + **kwargs + Keyword arguments forwarded to :any:`__call__`. + + Notes + ----- + This will store the field in the given mesh under the given name, + if a meshio or PyVista mesh was given. + + See: + - meshio: https://github.com/nschloe/meshio + - ogs5py: https://github.com/GeoStat-Framework/ogs5py + - PyVista: https://github.com/pyvista/pyvista + """ + return generate_on_mesh(self, mesh, points, direction, name, **kwargs) + + def pre_pos(self, pos=None, mesh_type="unstructured", info=False): + """ + Preprocessing positions and mesh_type. + + Parameters + ---------- + pos : :any:`iterable` + the position tuple, containing main direction and transversal + directions + mesh_type : :class:`str`, optional + 'structured' / 'unstructured' + Default: `"unstructured"` + info : :class:`bool`, optional + Whether to return information + + Returns + ------- + iso_pos : (d, n), :class:`numpy.ndarray` + Isometrized position tuple. + shape : :class:`tuple` + Shape of the resulting field. + info : :class:`dict`, optional + Information about settings. + + Warnings + -------- + When setting a new position tuple that differs from the present one, + all stored fields will be deleted. + """ + info_ret = {"deleted": False} + if pos is None: + if self.pos is None: + raise ValueError("Field: no position tuple 'pos' present") + else: + info_ret = self.set_pos(pos, mesh_type, info=True) + if self.mesh_type != "unstructured": + pos = generate_grid(self.pos) + else: + pos = self.pos + # return isometrized pos tuple, field shape and possible info + info_ret = (info_ret,) + if self.model is None: + return (pos, self.field_shape) + info * info_ret + return (self.model.isometrize(pos), self.field_shape) + info * info_ret + + def post_field(self, field, name="field", process=True, save=True): + """ + Postprocessing field values. + + Parameters + ---------- + field : :class:`numpy.ndarray` + Field values. + name : :class:`str`, optional + Name. to store the field. + The default is "field". + process : :class:`bool`, optional + Whether to process field to apply mean, normalizer and trend. + The default is True. + save : :class:`bool`, optional + Whether to store the field under the given name. + The default is True. + + Returns + ------- + field : :class:`numpy.ndarray` + Processed field values. + """ + if self.field_shape is None: + raise ValueError("post_field: no 'field_shape' present.") + field = np.asarray(field, dtype=np.double).reshape(self.field_shape) + if process: + field = apply_mean_norm_trend( + pos=self.pos, + field=field, + mesh_type=self.mesh_type, + value_type=self.value_type, + mean=self.mean, + normalizer=self.normalizer, + trend=self.trend, + check_shape=False, + stacked=False, + ) + if save: + name = str(name) + if not name.isidentifier() or ( + name not in self.field_names and name in dir(self) + ): + raise ValueError( + f"Field: given field name '{name}' is not valid" + ) + # allow resetting present fields + if name not in self._field_names: + self._field_names.append(name) + setattr(self, name, field) + return field + + def delete_fields(self, select=None): + """Delete selected fields.""" + del self[self.field_names if select is None else select] + + def transform( + self, method, field="field", store=True, process=False, **kwargs + ): + """ + Apply field transformation. + + Parameters + ---------- + method : :class:`str` + Method to use. + See :py:mod:`gstools.transform` for available transformations. + field : :class:`str`, optional + Name of field to be transformed. The default is "field". + store : :class:`str` or :class:`bool`, optional + Whether to store field inplace (True/False) or under a given name. + The default is True. + process : :class:`bool`, optional + Whether to process in/out fields with trend, normalizer and mean + of given Field instance. The default is False. + **kwargs + Keyword arguments forwarded to selected method. + + Raises + ------ + ValueError + When method is unknown. + + Returns + ------- + :class:`numpy.ndarray` + Transformed field. + """ + return apply( + self, method, field=field, store=store, process=process, **kwargs + ) + + def to_pyvista( + self, field_select="field", fieldname="field" + ): # pragma: no cover + """Create a VTK/PyVista grid of the stored field. + + Parameters + ---------- + field_select : :class:`str`, optional + Field that should be stored. Can be: + "field", "raw_field", "krige_field", "err_field" or "krige_var". + Default: "field" + fieldname : :class:`str`, optional + Name of the field in the VTK file. Default: "field" + """ + grid = to_vtk_helper( + self, filename=None, field_select=field_select, fieldname=fieldname + ) + return grid + + def vtk_export( + self, filename, field_select="field", fieldname="field" + ): # pragma: no cover + """Export the stored field to vtk. + + Parameters + ---------- + filename : :class:`str` + Filename of the file to be saved, including the path. Note that an + ending (.vtr or .vtu) will be added to the name. + field_select : :class:`str`, optional + Field that should be stored. Can be: + "field", "raw_field", "krige_field", "err_field" or "krige_var". + Default: "field" + fieldname : :class:`str`, optional + Name of the field in the VTK file. Default: "field" + """ + if not isinstance(filename, str): + raise TypeError("Please use a string filename.") + return to_vtk_helper( + self, + filename=filename, + field_select=field_select, + fieldname=fieldname, + ) + + def plot( + self, field="field", fig=None, ax=None, **kwargs + ): # pragma: no cover + """ + Plot the spatial random field. + + Parameters + ---------- + field : :class:`str`, optional + Field that should be plotted. + Default: "field" + fig : :class:`Figure` or :any:`None` + Figure to plot the axes on. If `None`, a new one will be created. + Default: `None` + ax : :class:`Axes` or :any:`None` + Axes to plot on. If `None`, a new one will be added to the figure. + Default: `None` + **kwargs + Forwarded to the plotting routine. + """ + # just import if needed; matplotlib is not required by setup + from gstools.field.plot import plot_field, plot_vec_field + + if self.value_type is None: + raise ValueError( + "Field value type not set! " + "Specify 'scalar' or 'vector' before plotting." + ) + + if self.value_type == "scalar": + r = plot_field(self, field, fig, ax, **kwargs) + elif self.value_type == "vector": + if self.dim == 2: + r = plot_vec_field(self, field, fig, ax, **kwargs) + else: + raise NotImplementedError( + "Streamflow plotting only supported for 2d case." + ) + else: + raise ValueError(f"Unknown field value type: {self.value_type}") + + return r + + def set_pos(self, pos, mesh_type="unstructured", info=False): + """ + Set positions and mesh_type. + + Parameters + ---------- + pos : :any:`iterable` + the position tuple, containing main direction and transversal + directions + mesh_type : :class:`str`, optional + 'structured' / 'unstructured' + Default: `"unstructured"` + info : :class:`bool`, optional + Whether to return information + + Returns + ------- + info : :class:`dict`, optional + Information about settings. + + Warnings + -------- + When setting a new position tuple that differs from the present one, + all stored fields will be deleted. + """ + info_ret = {"deleted": False} + old_type = copy(self.mesh_type) + old_pos = copy(self.pos) + # save pos and mesh-type + self.mesh_type = mesh_type + self.pos = pos + # remove present fields if new pos is different from current + if old_type != self.mesh_type or not _pos_equal(old_pos, self.pos): + self.delete_fields() + info_ret["deleted"] = True + del old_pos + return info_ret if info else None + + def get_store_config(self, store, default=None, fld_cnt=None): + """ + Get storage configuration from given selection. + + Parameters + ---------- + store : :class:`str` or :class:`bool` or :class:`list`, optional + Whether to store fields (True/False) with default names + or with specified names. + The default is :any:`True` for default names. + default : :class:`str` or :class:`list`, optional + Default field names. The default is "field". + fld_cnt : :any:`None` or :class:`int`, optional + Number of fields when using lists. The default is None. + + Returns + ------- + name : :class:`str` or :class:`list` + Name(s) of field. + save : :class:`bool` or :class:`list` + Whether to save field(s). + """ + if default is None: + if fld_cnt is None: + default = self.default_field_names[0] + else: + default = self.default_field_names + # single field + if fld_cnt is None: + save = isinstance(store, str) or bool(store) + name = store if isinstance(store, str) else default + return name, save + # multiple fields + default = _names(default, fld_cnt) + save = [True] * fld_cnt + if isinstance(store, str): + store = [store] + if isinstance(store, Iterable): + store = list(store)[:fld_cnt] + store += [True] * (fld_cnt - len(store)) + name = [None] * fld_cnt + for i, val in enumerate(store): + save[i] = isinstance(val, str) or bool(val) + name[i] = val if isinstance(val, str) else default[i] + else: + save = [bool(store)] * fld_cnt + name = copy(default) + return name, save + + @property + def pos(self): + """:class:`tuple`: The position tuple of the field.""" + return self._pos + + @pos.setter + def pos(self, pos): + if self.mesh_type == "unstructured": + self._pos = np.asarray(pos, dtype=np.double).reshape(self.dim, -1) + self._field_shape = np.shape(self._pos[0]) + else: + self._pos, self._field_shape = format_struct_pos_dim(pos, self.dim) + # prepend dimension if we have a vector field + if self.value_type == "vector": + self._field_shape = (self.dim,) + self._field_shape + if self.latlon: + raise ValueError("Field: Vector fields not allowed for latlon") + + @property + def all_fields(self): + """:class:`list`: All fields as stacked list.""" + return self[self.field_names] + + @property + def field_names(self): + """:class:`list`: Names of present fields.""" + return self._field_names + + @field_names.deleter + def field_names(self): + self.delete_fields() + + @property + def field_shape(self): + """:class:`tuple`: The shape of the field.""" + return self._field_shape + + @property + def mesh_type(self): + """:class:`str`: The mesh type of the field.""" + return self._mesh_type + + @mesh_type.setter + def mesh_type(self, mesh_type): + self._mesh_type = str(mesh_type) + + @property + def model(self): + """:any:`CovModel`: The covariance model of the field.""" + return self._model + + @model.setter + def model(self, model): + if model is not None: + if not isinstance(model, CovModel): + raise ValueError( + "Field: 'model' is not an instance of 'gstools.CovModel'" + ) + self._model = model + self._dim = None + elif self._dim is None: + raise ValueError("Field: either needs 'model' or 'dim'.") + else: + self._model = None + + @property + def mean(self): + """:class:`float` or :any:`callable`: The mean of the field.""" + return self._mean + + @mean.setter + def mean(self, mean): + self._mean = _set_mean_trend(mean, self.dim) + + @property + def normalizer(self): + """:any:`Normalizer`: Normalizer of the field.""" + return self._normalizer + + @normalizer.setter + def normalizer(self, normalizer): + self._normalizer = _check_normalizer(normalizer) + + @property + def trend(self): + """:class:`float` or :any:`callable`: The trend of the field.""" + return self._trend + + @trend.setter + def trend(self, trend): + self._trend = _set_mean_trend(trend, self.dim) + + @property + def value_type(self): + """:class:`str`: Type of the field values (scalar, vector).""" + return self._value_type + + @value_type.setter + def value_type(self, value_type): + if value_type not in self.valid_value_types: + raise ValueError( + f"Field: value type not in {self.valid_value_types}" + ) + self._value_type = value_type + + @property + def dim(self): + """:class:`int`: Dimension of the field.""" + return self._dim if self.model is None else self.model.field_dim + + @property + def latlon(self): + """:class:`bool`: Whether the field depends on geographical coords.""" + return False if self.model is None else self.model.latlon + + @property + def temporal(self): + """:class:`bool`: Whether the field depends on time.""" + return False if self.model is None else self.model.temporal + + @property + def name(self): + """:class:`str`: The name of the class.""" + return self.__class__.__name__ + + def _fmt_mean_norm_trend(self): + # fmt_mean_norm_trend for all child classes + return fmt_mean_norm_trend(self) + + def __repr__(self): + """Return String representation.""" + if self.model is None: + dim_str = f"dim={self.dim}" + else: + dim_str = f"model={self.model.name}" + return ( + f"{self.name}({dim_str}, " + f"value_type='{self.value_type}'{self._fmt_mean_norm_trend()})" + ) diff --git a/GSTools/source/src/gstools/field/cond_srf.py b/GSTools/source/src/gstools/field/cond_srf.py new file mode 100644 index 0000000000000000000000000000000000000000..aad26dba3ad17bb68b0253f8ccf8ebbd8ba6d396 --- /dev/null +++ b/GSTools/source/src/gstools/field/cond_srf.py @@ -0,0 +1,311 @@ +""" +GStools subpackage providing a class for conditioned spatial random fields. + +.. currentmodule:: gstools.field.cond_srf + +The following classes are provided + +.. autosummary:: + CondSRF +""" + +import numpy as np + +from gstools.field.base import Field +from gstools.field.generator import Generator, RandMeth +from gstools.krige import Krige + +__all__ = ["CondSRF"] + +GENERATOR = { + "RandMeth": RandMeth, +} +"""dict: Standard generators for conditioned spatial random fields.""" + + +class CondSRF(Field): + """A class to generate conditioned spatial random fields (SRF). + + Parameters + ---------- + krige : :any:`Krige` + Kriging setup to condition the spatial random field. + generator : :class:`str` or :any:`Generator`, optional + Name or class of the field generator to be used. + At the moment, only the following generator is provided: + + * "RandMeth" : The Randomization Method. + See: :any:`RandMeth` + + Default: "RandMeth" + **generator_kwargs + Keyword arguments that are forwarded to the generator in use. + Have a look at the provided generators for further information. + """ + + valid_value_types = ["scalar"] + """:class:`list` of :class:`str`: valid field value types.""" + + default_field_names = ["field", "raw_field", "raw_krige"] + """:class:`list`: Default field names.""" + + def __init__(self, krige, generator="RandMeth", **generator_kwargs): + if not isinstance(krige, Krige): + raise ValueError("CondSRF: krige should be an instance of Krige.") + self._krige = krige + # initialize attributes + self._field_names = [] + # initialize private attributes + self._generator = None + # initialize attributes + self.set_generator(generator, **generator_kwargs) + + def __call__( + self, + pos=None, + seed=np.nan, + mesh_type="unstructured", + post_process=True, + store=True, + krige_store=True, + **kwargs, + ): + """Generate the conditioned spatial random field. + + The field is saved as `self.field` and is also returned. + + Parameters + ---------- + pos : :class:`list`, optional + the position tuple, containing main direction and transversal + directions + seed : :class:`int`, optional + seed for RNG for resetting. Default: keep seed from generator + mesh_type : :class:`str` + 'structured' / 'unstructured' + post_process : :class:`bool`, optional + Whether to apply mean, normalizer and trend to the field. + Default: `True` + store : :class:`str` or :class:`bool` or :class:`list`, optional + Whether to store fields (True/False) with default names + or with specified names. + The default is :any:`True` for default names + ["field", "raw_field", "raw_krige"]. + krige_store : :class:`str` or :class:`bool` or :class:`list`, optional + Whether to store kriging fields (True/False) with default name + or with specified names. + The default is :any:`True` for default names + ["field", "krige_var"]. + **kwargs + keyword arguments that are forwarded to the kriging routine in use. + + Returns + ------- + field : :class:`numpy.ndarray` + the conditioned SRF + """ + name, save = self.get_store_config(store=store, fld_cnt=3) + krige_name, krige_save = self.krige.get_store_config( + store=krige_store, fld_cnt=2 + ) + kwargs["mesh_type"] = mesh_type + kwargs["only_mean"] = False # overwrite if given + kwargs["return_var"] = True # overwrite if given + kwargs["post_process"] = False # overwrite if given + kwargs["store"] = [False, krige_name[1] if krige_save[1] else False] + # update the model/seed in the generator if any changes were made + self.generator.update(self.model, seed) + # get isometrized positions and the resulting field-shape + iso_pos, shape, info = self.pre_pos(pos, mesh_type, info=True) + # generate the field + rawfield = np.reshape(self.generator(iso_pos, add_nugget=False), shape) + # call krige on already set pos (reuse already calculated fields) + if ( + not info["deleted"] + and name[2] in self.field_names + and krige_name[1] in self.krige.field_names + ): + reuse = True + rawkrige, krige_var = self[name[2]], self.krige[krige_name[1]] + else: + reuse = False + rawkrige, krige_var = self.krige(**kwargs) + var_scale, nugget = self.get_scaling(krige_var, shape) + # store krige field (need a copy to not alter field by reference) + if not reuse or krige_name[0] not in self.krige.field_names: + self.krige.post_field( + rawkrige.copy(), krige_name[0], post_process, krige_save[0] + ) + # store raw krige field + if not reuse: + self.post_field(rawkrige, name[2], False, save[2]) + # store raw random field + self.post_field(rawfield, name[1], False, save[1]) + # store cond random field + return self.post_field( + field=rawkrige + var_scale * rawfield + nugget, + name=name[0], + process=post_process, + save=save[0], + ) + + def get_scaling(self, krige_var, shape): + """ + Get scaling coefficients for the random field. + + Parameters + ---------- + krige_var : :class:`numpy.ndarray` + Kriging variance. + shape : :class:`tuple` of :class:`int` + Field shape. + + Returns + ------- + var_scale : :class:`numpy.ndarray` + Variance scaling factor for the random field. + nugget : :class:`numpy.ndarray` or :class:`int` + Nugget to be added to the field. + """ + if self.model.nugget > 0: + var_scale = np.maximum(krige_var - self.model.nugget, 0) + nug_scale = np.sqrt((krige_var - var_scale) / self.model.nugget) + var_scale = np.sqrt(var_scale / self.model.var) + nugget = nug_scale * self.generator.get_nugget(shape) + else: + var_scale = np.sqrt(krige_var / self.model.var) + nugget = 0 + return var_scale, nugget + + def set_generator(self, generator, **generator_kwargs): + """Set the generator for the field. + + Parameters + ---------- + generator : :class:`str` or :any:`Generator`, optional + Name or class of the generator to use for field generation. + Default: "RandMeth" + **generator_kwargs + keyword arguments that are forwarded to the generator in use. + """ + gen = GENERATOR[generator] if generator in GENERATOR else generator + if not (isinstance(gen, type) and issubclass(gen, Generator)): + raise ValueError( + f"gstools.CondSRF: Unknown or wrong generator: {generator}" + ) + self._generator = gen(self.model, **generator_kwargs) + self.value_type = self.generator.value_type + + def set_pos(self, pos, mesh_type="unstructured", info=False): + """ + Set positions and mesh_type. + + Parameters + ---------- + pos : :any:`iterable` + the position tuple, containing main direction and transversal + directions + mesh_type : :class:`str`, optional + 'structured' / 'unstructured' + Default: `"unstructured"` + info : :class:`bool`, optional + Whether to return information + + Returns + ------- + info : :class:`dict`, optional + Information about settings. + + Warnings + -------- + When setting a new position tuple that differs from the present one, + all stored fields will be deleted. + """ + info_ret = super().set_pos(pos, mesh_type, info=True) + if info_ret["deleted"]: + self.krige.delete_fields() + return info_ret if info else None + + @property + def pos(self): + """:class:`tuple`: The position tuple of the field.""" + return self.krige.pos + + @pos.setter + def pos(self, pos): + self.krige.pos = pos + + @property + def field_shape(self): + """:class:`tuple`: The shape of the field.""" + return self.krige.field_shape + + @property + def mesh_type(self): + """:class:`str`: The mesh type of the field.""" + return self.krige.mesh_type + + @mesh_type.setter + def mesh_type(self, mesh_type): + self.krige.mesh_type = mesh_type + + @property + def krige(self): + """:any:`Krige`: The underlying kriging class.""" + return self._krige + + @property + def generator(self): + """:any:`callable`: The generator of the field.""" + return self._generator + + @property + def model(self): + """:any:`CovModel`: The covariance model of the field.""" + return self.krige.model + + @model.setter + def model(self, model): + self.krige.model = model + + @property + def mean(self): + """:class:`float` or :any:`callable`: The mean of the field.""" + return self.krige.mean + + @mean.setter + def mean(self, mean): + self.krige.mean = mean + + @property + def normalizer(self): + """:any:`Normalizer`: Normalizer of the field.""" + return self.krige.normalizer + + @normalizer.setter + def normalizer(self, normalizer): + self.krige.normalizer = normalizer + + @property + def trend(self): + """:class:`float` or :any:`callable`: The trend of the field.""" + return self.krige.trend + + @trend.setter + def trend(self, trend): + self.krige.trend = trend + + @property + def value_type(self): + """:class:`str`: Type of the field values (scalar, vector).""" + return self.krige.value_type + + @value_type.setter + def value_type(self, value_type): + self.krige.value_type = value_type + + def __repr__(self): + """Return String representation.""" + return ( + f"{self.name}(krige={self.krige}, generator={self.generator.name})" + ) diff --git a/GSTools/source/src/gstools/field/generator.py b/GSTools/source/src/gstools/field/generator.py new file mode 100644 index 0000000000000000000000000000000000000000..d65cc3cd7c152834c4dc256e751281aa08218db6 --- /dev/null +++ b/GSTools/source/src/gstools/field/generator.py @@ -0,0 +1,919 @@ +""" +GStools subpackage providing generators for spatial random fields. + +.. currentmodule:: gstools.field.generator + +The following classes are provided + +.. autosummary:: + :toctree: + + Generator + RandMeth + IncomprRandMeth + Fourier +""" + +import warnings +from abc import ABC, abstractmethod +from copy import deepcopy as dcp + +import numpy as np +from gstools_cython.field import summate as summate_c +from gstools_cython.field import summate_fourier as summate_fourier_c +from gstools_cython.field import summate_incompr as summate_incompr_c + +from gstools import config +from gstools.covmodel.base import CovModel +from gstools.random.rng import RNG +from gstools.tools.geometric import generate_grid + +if config._GSTOOLS_CORE_AVAIL: # pragma: no cover + from gstools_core import summate as summate_gsc + from gstools_core import summate_fourier as summate_fourier_gsc + from gstools_core import summate_incompr as summate_incompr_gsc + +__all__ = ["Generator", "RandMeth", "IncomprRandMeth", "Fourier"] + + +SAMPLING = ["auto", "inversion", "mcmc"] + + +def _summate(cov_samples, z_1, z_2, pos, num_threads=None): + """A wrapper function for calling the randomization algorithms.""" + if config.USE_GSTOOLS_CORE and config._GSTOOLS_CORE_AVAIL: + summate_fct = summate_gsc + else: + summate_fct = summate_c + return summate_fct(cov_samples, z_1, z_2, pos, num_threads) + + +def _summate_incompr( + cov_samples, + z_1, + z_2, + pos, + num_threads=None, +): + """A wrapper function for calling the incompr. randomization algorithms.""" + + if config.USE_GSTOOLS_CORE and config._GSTOOLS_CORE_AVAIL: + summate_incompr_fct = summate_incompr_gsc + else: + summate_incompr_fct = summate_incompr_c + return summate_incompr_fct(cov_samples, z_1, z_2, pos, num_threads) + + +def _summate_fourier(spectrum_factor, modes, z_1, z_2, pos, num_threads=None): + """A wrapper function for calling the Fourier algorithms.""" + if config.USE_GSTOOLS_CORE and config._GSTOOLS_CORE_AVAIL: + summate_fourier_fct = summate_fourier_gsc + else: + summate_fourier_fct = summate_fourier_c + return summate_fourier_fct( + spectrum_factor, modes, z_1, z_2, pos, num_threads + ) + + +class Generator(ABC): + """ + Abstract generator class. + + Parameters + ---------- + model : :any:`CovModel` + Covariance model + **kwargs + Placeholder for keyword-args + """ + + @abstractmethod + def __init__(self, model, **kwargs): + pass + + @abstractmethod + def update(self, model=None, seed=np.nan): + """Update the model and the seed. + + If model and seed are not different, nothing will be done. + + Parameters + ---------- + model : :any:`CovModel` or :any:`None`, optional + covariance model. Default: :any:`None` + seed : :class:`int` or :any:`None` or :any:`numpy.nan`, optional + the seed of the random number generator. + If :any:`None`, a random seed is used. If :any:`numpy.nan`, + the actual seed will be kept. Default: :any:`numpy.nan` + """ + + @abstractmethod + def get_nugget(self, shape): + """ + Generate normal distributed values for the nugget simulation. + + Parameters + ---------- + shape : :class:`tuple` + the shape of the summed modes + + Returns + ------- + nugget : :class:`numpy.ndarray` + the nugget in the same shape as the summed modes + """ + + @abstractmethod + def __call__(self, pos, add_nugget=True): + """ + Generate the field. + + Parameters + ---------- + pos : (d, n), :class:`numpy.ndarray` + the position tuple with d dimensions and n points. + add_nugget : :class:`bool` + Whether to add nugget noise to the field. + + Returns + ------- + :class:`numpy.ndarray` + the random modes + """ + + @property + @abstractmethod + def model(self): + """:any:`CovModel`: Covariance model of the spatial random field.""" + + @property + def zero_var(self): + """:class:`bool`: Whether Covariance model has zero variance.""" + return np.isclose(self.model.var, 0) + + @property + @abstractmethod + def value_type(self): + """:class:`str`: Type of the field values (scalar, vector).""" + + @property + def name(self): + """:class:`str`: Name of the generator.""" + return self.__class__.__name__ + + +class RandMeth(Generator): + r"""Randomization method for calculating isotropic random fields. + + Parameters + ---------- + model : :any:`CovModel` + Covariance model + mode_no : :class:`int`, optional + Number of Fourier modes. Default: ``1000`` + seed : :class:`int` or :any:`None`, optional + The seed of the random number generator. + If "None", a random seed is used. Default: :any:`None` + sampling : :class:`str`, optional + Sampling strategy. Either + + * "auto": select best strategy depending on given model + * "inversion": use inversion method + * "mcmc": use mcmc sampling + + **kwargs + Placeholder for keyword-args + + Notes + ----- + The Randomization method is used to generate isotropic + spatial random fields characterized by a given covariance model. + The calculation looks like [Hesse2014]_: + + .. math:: + u\left(x\right)= + \sqrt{\frac{\sigma^{2}}{N}}\cdot + \sum_{i=1}^{N}\left( + Z_{1,i}\cdot\cos\left(\left\langle k_{i},x\right\rangle \right)+ + Z_{2,i}\cdot\sin\left(\left\langle k_{i},x\right\rangle \right) + \right) + + where: + + * :math:`N` : fourier mode number + * :math:`Z_{j,i}` : random samples from a normal distribution + * :math:`k_i` : samples from the spectral density distribution of + the covariance model + + References + ---------- + .. [Hesse2014] Heße, F., Prykhodko, V., Schlüter, S., and Attinger, S., + "Generating random fields with a truncated power-law variogram: + A comparison of several numerical methods", + Environmental Modelling & Software, 55, 32-48., (2014) + """ + + def __init__( + self, + model, + *, + mode_no=1000, + seed=None, + sampling="auto", + **kwargs, + ): + if kwargs: + warnings.warn("gstools.RandMeth: **kwargs are ignored") + # initialize attributes + self._mode_no = int(mode_no) + # initialize private attributes + self._model = None + self._seed = None + self._rng = None + self._z_1 = None + self._z_2 = None + self._cov_sample = None + self._value_type = "scalar" + # set sampling strategy + self._sampling = None + self.sampling = sampling + # set model and seed + self.update(model, seed) + + def __call__(self, pos, add_nugget=True): + """Calculate the random modes for the randomization method. + + This method calls the `summate_*` Rust or Cython methods, which are + the heart of the randomization method. + + Parameters + ---------- + pos : (d, n), :class:`numpy.ndarray` + the position tuple with d dimensions and n points. + add_nugget : :class:`bool` + Whether to add nugget noise to the field. + + Returns + ------- + :class:`numpy.ndarray` + the random modes + """ + pos = np.asarray(pos, dtype=np.double) + if self.zero_var: + shp = pos.shape[1:] + return self.get_nugget(shp) if add_nugget else np.full(shp, 0.0) + # generate if var is not 0 + summed_modes = _summate( + self._cov_sample, self._z_1, self._z_2, pos, config.NUM_THREADS + ) + nugget = self.get_nugget(summed_modes.shape) if add_nugget else 0.0 + return np.sqrt(self.model.var / self._mode_no) * summed_modes + nugget + + def get_nugget(self, shape): + """ + Generate normal distributed values for the nugget simulation. + + Parameters + ---------- + shape : :class:`tuple` + the shape of the summed modes + + Returns + ------- + nugget : :class:`numpy.ndarray` + the nugget in the same shape as the summed modes + """ + if self.model.nugget > 0: + nugget = np.sqrt(self.model.nugget) * self._rng.random.normal( + size=shape + ) + else: + nugget = np.full(shape, 0.0) if self.zero_var else 0.0 + return nugget + + def update(self, model=None, seed=np.nan): + """Update the model and the seed. + + If model and seed are not different, nothing will be done. + + Parameters + ---------- + model : :any:`CovModel` or :any:`None`, optional + covariance model. Default: :any:`None` + seed : :class:`int` or :any:`None` or :any:`numpy.nan`, optional + the seed of the random number generator. + If :any:`None`, a random seed is used. If :any:`numpy.nan`, + the actual seed will be kept. Default: :any:`numpy.nan` + """ + # check if a new model is given + if isinstance(model, CovModel): + if self.model != model: + self._model = dcp(model) + if seed is None or not np.isnan(seed): + self.reset_seed(seed) + else: + self.reset_seed(self._seed) + # just update the seed, if its a new one + elif seed is None or not np.isnan(seed): + self.seed = seed + # or just update the seed, when no model is given + elif model is None and (seed is None or not np.isnan(seed)): + if isinstance(self._model, CovModel): + self.seed = seed + else: + raise ValueError( + "gstools.field.generator.RandMeth: no 'model' given" + ) + # if the user tries to trick us, we beat them! + elif model is None and np.isnan(seed): + if not ( + isinstance(self._model, CovModel) + and self._z_1 is not None + and self._z_2 is not None + and self._cov_sample is not None + ): + raise ValueError( + "gstools.field.generator.RandMeth: " + "neither 'model' nor 'seed' given!" + ) + # wrong model type + else: + raise ValueError( + "gstools.field.generator.RandMeth: 'model' is not an " + "instance of 'gstools.CovModel'" + ) + + def reset_seed(self, seed=np.nan): + """ + Recalculate the random amplitudes and wave numbers with the given seed. + + Parameters + ---------- + seed : :class:`int` or :any:`None` or :any:`numpy.nan`, optional + the seed of the random number generator. + If :any:`None`, a random seed is used. If :any:`numpy.nan`, + the actual seed will be kept. Default: :any:`numpy.nan` + + Notes + ----- + Even if the given seed is the present one, modes will be recalculated. + """ + if seed is None or not np.isnan(seed): + self._seed = seed + self._rng = RNG(self._seed) + # normal distributed samples for randmeth + self._z_1 = self._rng.random.normal(size=self._mode_no) + self._z_2 = self._rng.random.normal(size=self._mode_no) + # sample uniform on a sphere + if self.zero_var: + self._cov_sample = np.full((self.model.dim, self._mode_no), 0.0) + else: + sph_crd = self._rng.sample_sphere(self.model.dim, self._mode_no) + # sample radii according to radial spectral density of the model + if self.sampling == "inversion" or ( + self.sampling == "auto" and self.model.has_ppf + ): + pdf, cdf, ppf = self.model.dist_func + rad = self._rng.sample_dist( + size=self._mode_no, pdf=pdf, cdf=cdf, ppf=ppf, a=0 + ) + else: + rad = self._rng.sample_ln_pdf( + ln_pdf=self.model.ln_spectral_rad_pdf, + size=self._mode_no, + sample_around=1.0 / self.model.len_rescaled, + ) + # get spatial samples by multiplying sphere samples and radii + self._cov_sample = rad * sph_crd + + @property + def sampling(self): + """:class:`str`: Sampling strategy.""" + return self._sampling + + @sampling.setter + def sampling(self, sampling): + if sampling not in ["auto", "inversion", "mcmc"]: + raise ValueError(f"RandMeth: sampling not in {SAMPLING}.") + self._sampling = sampling + + @property + def seed(self): + """:class:`int`: Seed of the master RNG. + + Notes + ----- + If a new seed is given, the setter property not only saves the + new seed, but also creates new random modes with the new seed. + """ + return self._seed + + @seed.setter + def seed(self, new_seed): + if new_seed is not self._seed: + self.reset_seed(new_seed) + + @property + def model(self): + """:any:`CovModel`: Covariance model of the spatial random field.""" + return self._model + + @model.setter + def model(self, model): + self.update(model) + + @property + def mode_no(self): + """:class:`int`: Number of modes in the randomization method.""" + return self._mode_no + + @mode_no.setter + def mode_no(self, mode_no): + if int(mode_no) != self._mode_no: + self._mode_no = int(mode_no) + self.reset_seed(self._seed) + + @property + def value_type(self): + """:class:`str`: Type of the field values (scalar, vector).""" + return self._value_type + + def __repr__(self): + """Return String representation.""" + return ( + f"{self.name}(model={self.model}, " + f"mode_no={self._mode_no}, seed={self.seed})" + ) + + +class IncomprRandMeth(RandMeth): + r"""RandMeth for incompressible random vector fields. + + Parameters + ---------- + model : :any:`CovModel` + covariance model + mean_velocity : :class:`float`, optional + the mean velocity in x-direction + mode_no : :class:`int`, optional + number of Fourier modes. Default: ``1000`` + seed : :class:`int` or :any:`None`, optional + the seed of the random number generator. + If "None", a random seed is used. Default: :any:`None` + sampling : :class:`str`, optional + Sampling strategy. Either + + * "auto": select best strategy depending on given model + * "inversion": use inversion method + * "mcmc": use mcmc sampling + + **kwargs + Placeholder for keyword-args + + Notes + ----- + The Randomization method is used to generate isotropic + spatial incompressible random vector fields characterized + by a given covariance model. The equation is [Kraichnan1970]_: + + .. math:: + u_i\left(x\right)= \bar{u_i} \delta_{i1} + + \bar{u_i}\sqrt{\frac{\sigma^{2}}{N}}\cdot + \sum_{j=1}^{N}p_i(k_{j})\left( + Z_{1,j}\cdot\cos\left(\left\langle k_{j},x\right\rangle \right)+ + Z_{2,j}\cdot\sin\left(\left\langle k_{j},x\right\rangle \right) + \right) + + where: + + * :math:`\bar u` : mean velocity in :math:`e_1` direction + * :math:`N` : fourier mode number + * :math:`Z_{k,j}` : random samples from a normal distribution + * :math:`k_j` : samples from the spectral density distribution of + the covariance model + * :math:`p_i(k_j) = e_1 - \frac{k_i k_1}{k^2}` : the projector + ensuring the incompressibility + + References + ---------- + .. [Kraichnan1970] Kraichnan, R. H., + "Diffusion by a random velocity field.", + The physics of fluids, 13(1), 22-31., (1970) + """ + + def __init__( + self, + model, + *, + mean_velocity=1.0, + mode_no=1000, + seed=None, + sampling="auto", + **kwargs, + ): + if model.dim < 2 or model.dim > 3: + raise ValueError( + "Only 2D and 3D incompressible fields can be generated." + ) + super().__init__( + model=model, + mode_no=mode_no, + seed=seed, + sampling=sampling, + **kwargs, + ) + + self.mean_u = mean_velocity + self._value_type = "vector" + + def __call__(self, pos, add_nugget=True): + """Calculate the random modes for the randomization method. + + This method calls the `summate_incompr_*` Rust or Cython methods, + which are the heart of the randomization method. + In this class the method contains a projector to + ensure the incompressibility of the vector field. + + Parameters + ---------- + pos : (d, n), :class:`numpy.ndarray` + the position tuple with d dimensions and n points. + add_nugget : :class:`bool` + Whether to add nugget noise to the field. + + Returns + ------- + :class:`numpy.ndarray` + the random modes + """ + pos = np.asarray(pos, dtype=np.double) + nugget = self.get_nugget(pos.shape) if add_nugget else 0.0 + e1 = self._create_unit_vector(pos.shape) + if self.zero_var: + return self.mean_u * e1 + nugget + summed_modes = _summate_incompr( + self._cov_sample, + self._z_1, + self._z_2, + pos, + config.NUM_THREADS, + ) + return ( + self.mean_u * e1 + + self.mean_u + * np.sqrt(self.model.var / self._mode_no) + * summed_modes + + nugget + ) + + def _create_unit_vector(self, broadcast_shape, axis=0): + """Create a unit vector. + + Can be multiplied with a vector of shape broadcast_shape + + Parameters + ---------- + broadcast_shape : :class:`tuple` + the shape of the array with which + the unit vector is to be multiplied + axis : :class:`int`, optional + the direction of the unit vector. Default: ``0`` + + Returns + ------- + :class:`numpy.ndarray` + the unit vector + """ + shape = np.ones(len(broadcast_shape), dtype=int) + shape[0] = self.model.dim + + e1 = np.zeros(shape) + e1[axis] = 1.0 + return e1 + + +class Fourier(Generator): + r"""Fourier method for calculating periodic, isotropic random fields. + + Parameters + ---------- + model : :any:`CovModel` + Covariance model + period : :class:`list` or :class:`float` + The spatial periodicity of the field, is often the domain size. + mode_no : :class:`list` or :class:`float`, optional + Number of Fourier modes per dimension. + seed : :class:`int`, optional + The seed of the random number generator. + If "None", a random seed is used. Default: :any:`None` + + **kwargs + Placeholder for keyword-args + + Notes + ----- + The Fourier method is used to generate periodic isotropic spatial random + fields characterized by a given covariance model. + The calculation looks like: + + .. math:: + u\left(x\right)= + \sum_{i=1}^{N}\sqrt{2S(k_{i})\Delta k}\left( + Z_{1,i}\cdot\cos\left(\left\langle k_{i},x\right\rangle \right)+ + Z_{2,i}\cdot\sin\left(\left\langle k_{i},x\right\rangle \right) + \right) + + where: + + * :math:`S` : spectrum of the covariance model + * :math:`Z_{j,i}` : random samples from a normal distribution + * :math:`k_i` : the equidistant Fourier grid + """ + + def __init__( + self, + model, + period, + mode_no=32, + seed=None, + **kwargs, + ): + if kwargs: + warnings.warn("gstools.Fourier: **kwargs are ignored") + + # initialize private attributes + self._modes = None + self._period = None + self._mode_no = None + self._delta_k = None + self._model = None + self._seed = None + self._rng = None + self._z_1 = None + self._z_2 = None + self._spectrum_factor = None + self._value_type = "scalar" + # set model and seed + self.update(model, seed, period, mode_no) + + def __call__(self, pos, add_nugget=True): + """Calculate the modes for the Fourier method. + + This method calls the `summate_fourier` Cython method, which is the + heart of the Fourier method. + + Parameters + ---------- + pos : (d, n), :class:`numpy.ndarray` + the position tuple with d dimensions and n points. + add_nugget : :class:`bool` + Whether to add nugget noise to the field. + + Returns + ------- + :class:`numpy.ndarray` + the random modes + """ + pos = np.asarray(pos, dtype=np.double) + if self.zero_var: + shp = pos.shape[1:] + return self.get_nugget(shp) if add_nugget else np.full(shp, 0.0) + # generate if var is not 0 + summed_modes = _summate_fourier( + self._spectrum_factor, + self._modes, + self._z_1, + self._z_2, + pos, + config.NUM_THREADS, + ) + nugget = self.get_nugget(summed_modes.shape) if add_nugget else 0.0 + return summed_modes + nugget + + def get_nugget(self, shape): + """ + Generate normal distributed values for the nugget simulation. + + Parameters + ---------- + shape : :class:`tuple` + the shape of the summed modes + + Returns + ------- + nugget : :class:`numpy.ndarray` + the nugget in the same shape as the summed modes + """ + if self.model.nugget > 0: + nugget = np.sqrt(self.model.nugget) * self._rng.random.normal( + size=shape + ) + else: + nugget = np.full(shape, 0.0) if self.zero_var else 0.0 + return nugget + + def update(self, model=None, seed=np.nan, period=None, mode_no=None): + """Update the model and the seed. + + If model and seed are not different, nothing will be done. + + Parameters + ---------- + model : :any:`CovModel` or :any:`None`, optional + covariance model. Default: :any:`None` + seed : :class:`int` or :any:`None` or :any:`numpy.nan`, optional + the seed of the random number generator. + If :any:`None`, a random seed is used. If :any:`numpy.nan`, + the actual seed will be kept. Default: :any:`numpy.nan` + period : :class:`list` or :any:`None`, optional + The spatial periodicity of the field, is often the domain size. + mode_no : :class:`list` or :any:`None`, optional + Number of Fourier modes per dimension. + """ + tmp_model = model if model is not None else self._model + dim = tmp_model.dim + if period is not None: + self._period = self._fill_to_dim(period, dim) + anis = np.insert(tmp_model.anis.copy(), 0, 1.0) + self._delta_k = 2.0 * np.pi / self._period * anis + if mode_no is None: + self._set_modes(self._mode_no, dim) + if mode_no is not None: + mode_no = self._fill_to_dim(mode_no, dim) + if (np.asarray([m % 2 for m in mode_no]) != 0).any(): + raise ValueError("Fourier: Odd mode_no not supported.") + self._set_modes(mode_no, dim) + + # check if a new model is given + if isinstance(model, CovModel): + if self.model != model: + self._model = dcp(model) + if seed is None or not np.isnan(seed): + self.reset_seed(seed) + else: + self.reset_seed(self._seed) + # just update the seed, if its a new one + elif seed is None or not np.isnan(seed): + self.seed = seed + # or just update the seed, when no model is given + elif model is None and (seed is None or not np.isnan(seed)): + if isinstance(self._model, CovModel): + self.seed = seed + else: + raise ValueError( + "gstools.field.generator.Fourier: no 'model' given" + ) + # but also update when mode mesh was modified + elif mode_no is not None or period is not None: + if seed is None or not np.isnan(seed): + self.reset_seed(seed) + else: + self.reset_seed(self._seed) + # if the user tries to trick us, we beat them! + elif model is None and np.isnan(seed): + if ( + isinstance(self._model, CovModel) + and self._z_1 is not None + and self._z_2 is not None + and self._spectrum_factor is not None + ): + raise ValueError( + "gstools.field.generator.Fourier: " + "neither 'model' nor 'seed' given!" + ) + # wrong model type + else: + raise ValueError( + "gstools.field.generator.Fourier: 'model' is not an " + "instance of 'gstools.CovModel'" + ) + + def reset_seed(self, seed=np.nan): + """ + Recalculate the random values with the given seed. + + Parameters + ---------- + seed : :class:`int` or :any:`None` or :any:`numpy.nan`, optional + the seed of the random number generator. + If :any:`None`, a random seed is used. If :any:`numpy.nan`, + the actual seed will be kept. Default: :any:`numpy.nan` + + Notes + ----- + Even if the given seed is the present one, modes will be recalculated. + """ + if seed is None or not np.isnan(seed): + self._seed = seed + self._rng = RNG(self._seed) + # normal distributed samples for randmeth + self._z_1 = self._rng.random.normal(size=np.prod(self._mode_no)) + self._z_2 = self._rng.random.normal(size=np.prod(self._mode_no)) + # pre calc. the spectrum for all wave numbers they are handed over to + # Cython, which doesn't have access to the CovModel + if self.zero_var: + self._spectrum_factor = np.full(np.prod(self._mode_no), 0.0) + else: + k_norm = np.linalg.norm(self._modes, axis=0) + self._spectrum_factor = np.sqrt( + self._model.spectrum(k_norm) * np.prod(self._delta_k) + ) + + def _fill_to_dim(self, values, dim, dtype=float, default_value=None): + """Fill an array with last element up to len(dim).""" + r = np.atleast_1d(values) + if values is None: + if default_value is None: + raise ValueError("Fourier: Value has to be provided") + r = default_value + r = np.array(r, dtype=dtype) + r = np.atleast_1d(r)[:dim] + if len(r) > dim: + raise ValueError(f"Fourier: len(values) <= {dim=} not fulfilled") + # fill up values with values[-1], such that len()==dim + if len(r) < dim: + r = np.pad(r, (0, dim - len(r)), "edge") + return r + + def _set_modes(self, mode_no, dim): + """Calculate the mode mesh. + + Parameters + ---------- + mode_no : :class:`list` + Number of Fourier modes per dimension. + dim : :class:`int` + dimension of the model. + + Notes + ----- + `self._reset_seed` *has* to be called after this method! + """ + modes = [ + np.arange( + -mode_no[d] / 2.0 * self._delta_k[d], + mode_no[d] / 2.0 * self._delta_k[d], + self._delta_k[d], + ) + for d in range(dim) + ] + # initialize attributes + self._modes = generate_grid(modes) + self._mode_no = [len(m) for m in modes] + + @property + def seed(self): + """:class:`int`: Seed of the master RNG. + + Notes + ----- + If a new seed is given, the setter property not only saves the + new seed, but also creates new random modes with the new seed. + """ + return self._seed + + @seed.setter + def seed(self, new_seed): + if new_seed is not self._seed: + self.reset_seed(new_seed) + + @property + def model(self): + """:any:`CovModel`: Covariance model of the spatial random field.""" + return self._model + + @model.setter + def model(self, model): + self.update(model) + + @property + def modes(self): + """:class:`numpy.ndarray`: Modes on which the spectrum is calculated.""" + return self._modes + + @property + def mode_no(self): + """:class:`numpy.ndarray`: Number of modes per dimension.""" + return self._mode_no + + @mode_no.setter + def mode_no(self, mode_no): + self.update(mode_no=mode_no) + + @property + def period(self): + """:class:`numpy.ndarray`: Period length of the spatial random field.""" + return self._period + + @period.setter + def period(self, period): + self.update(period=period) + + @property + def value_type(self): + """:class:`str`: Type of the field values (scalar, vector).""" + return self._value_type + + def __repr__(self): + """Return String representation.""" + return f"{self.name}(model={self.model}, seed={self.seed})" diff --git a/GSTools/source/src/gstools/field/pgs.py b/GSTools/source/src/gstools/field/pgs.py new file mode 100644 index 0000000000000000000000000000000000000000..40fcef7a6a575f6b61f25c6096dc8f2cf3b31ef3 --- /dev/null +++ b/GSTools/source/src/gstools/field/pgs.py @@ -0,0 +1,172 @@ +""" +GStools subpackage providing plurigaussian simulation (PGS) + +.. currentmodule:: gstools.field.pgs + +The following classes are provided + +.. autosummary:: + :toctree: + + PGS +""" + +import numpy as np + +# very clunky way of supporting both np 1.x and 2.x exceptions +try: + np.AxisError = np.exceptions.AxisError +except AttributeError: + ... + + +class PGS: + """A class to generate plurigaussian field simulations (PGS). + + See e.g. [Ricketts2023]_ and [Armstrong2011]_ for more details. + + Parameters + ---------- + dim : :class:`int` + dimension of the field + fields : :class:`list` or :class:`numpy.ndarray` + For `dim > 1` a list of spatial random fields (SRFs), with + `len(fields) == dim`. For `dim == 1`, the SRF can be directly given, + instead of a list. This class supports structured and unstructured meshes. + All fields must have the same shapes. + + Notes + ----- + Using plurigaussian fields for conditioning fields is still a beta feature. + + References + ---------- + .. [Ricketts2023] Ricketts, E.J., Freeman, B.L., Cleall, P.J. et al. + A Statistical Finite Element Method Integrating a Plurigaussian Random + Field Generator for Multi-scale Modelling of Solute Transport in + Concrete. Transp Porous Med 148, 95–121 (2023) + https://doi.org/10.1007/s11242-023-01930-8 + .. [Armstrong2011] Armstrong, Margaret, et al. + Plurigaussian simulations in geosciences. + Springer Science & Business Media, 2011. + https://doi.org/10.1007/978-3-642-19607-2 + """ + + def __init__(self, dim, fields): + # hard to test for 1d case + if dim > 1: + if dim != len(fields): + raise ValueError( + "PGS: Mismatch between dim. and no. of fields." + ) + for d in range(1, dim): + if not fields[0].shape == fields[d].shape: + raise ValueError("PGS: Not all fields have the same shape.") + self._dim = dim + self._fields = np.array(fields) + self._lithotypes = None + self._pos_lith = None + try: + self._mapping = np.stack(self._fields, axis=1) + except np.AxisError: + # if dim==1, `fields` is prob. a raw field & not a 1-tuple or + # equivalent + if self._dim == 1: + self._fields = np.array([self._fields]) + self._mapping = np.stack(self._fields, axis=1) + else: + raise + + def __call__(self, lithotypes): + """Generate the plurigaussian field. + + Parameters + ---------- + lithotypes : :class:`numpy.ndarray` + A `dim` dimensional structured field, whose values are mapped to the PGS. + It does not have to have the same shape as the `fields`, as the indices are + automatically scaled. + Returns + ------- + pgs : :class:`numpy.ndarray` + the plurigaussian field + """ + self._lithotypes = np.array(lithotypes) + if len(self._lithotypes.shape) != self._dim: + raise ValueError("PGS: Mismatch between dim. and facies shape.") + self._pos_lith = self.calc_lithotype_axes(self._lithotypes.shape) + + P_dig = [] + for d in range(self._dim): + P_dig.append(np.digitize(self._mapping[:, d], self._pos_lith[d])) + + # once Py3.11 has reached its EOL, we can drop the 1-tuple :-) + return self._lithotypes[(*P_dig,)] + + def calc_lithotype_axes(self, lithotypes_shape): + """Calculate the axes on which the lithorypes are defined. + + With the centroid of the correlations of the SRFs at the center, + the axes are calculated, which hold all correlations. + These axes are used for the lithotypes field. + + Parameters + ---------- + lithotypes_shape : :class:`tuple` + The shape of the lithotypes field. + + Returns + ------- + pos_lith : :class:`numpy.ndarray` + the axes holding all field correlations + """ + pos_lith = [] + try: + # structured grid + centroid = self._fields.mean(axis=tuple(range(1, self._dim + 1))) + except np.AxisError: + # unstructured grid + centroid = self._fields.mean(axis=1) + for d in range(self._dim): + l = np.floor(self._fields[d].min()) - 1 # noqa: E741 + h = np.ceil(self._fields[d].max()) + 1 + m = (h + l) / 2.0 + dist = max(np.abs(h - m), np.abs(l - m)) + pos_lith.append( + np.linspace( + centroid[d] - dist, + centroid[d] + dist, + lithotypes_shape[d], + ) + ) + return pos_lith + + def transform_coords(self, lithotypes_shape, pos): + """Transform position from correlation coords to L indices. + + This is a helper method to get the lithoty pes indices for given + correlated field values. + + Parameters + ---------- + lithotypes_shape : :class:`tuple` + The shape of the lithotypes field. + pos : :class:`numpy.ndarray` + The position in field coordinates, which will be transformed. + + Returns + ------- + pos_trans : :class:`list` + the transformed position tuple + """ + pos_trans = [] + pos_lith = self.calc_lithotype_axes(lithotypes_shape) + for d in range(self._dim): + pos_trans.append( + int( + (pos[d] - pos_lith[d][0]) + / (pos_lith[d][-1] - pos_lith[d][0]) + * lithotypes_shape[d] + ) + ) + return pos_trans diff --git a/GSTools/source/src/gstools/field/plot.py b/GSTools/source/src/gstools/field/plot.py new file mode 100644 index 0000000000000000000000000000000000000000..b36b048a54fc699740a71868ac5cf434f8f930c5 --- /dev/null +++ b/GSTools/source/src/gstools/field/plot.py @@ -0,0 +1,401 @@ +""" +GStools subpackage providing plotting routines for spatial fields. + +.. currentmodule:: gstools.field.plot + +The following classes and functions are provided + +.. autosummary:: + plot_field + plot_vec_field +""" + +import numpy as np +from scipy import interpolate as inter +from scipy.spatial import ConvexHull + +from gstools.tools.geometric import rotation_planes +from gstools.tools.misc import get_fig_ax + +try: + import matplotlib.pyplot as plt + from matplotlib.widgets import RadioButtons, Slider +except ImportError as exc: + raise ImportError("Plotting: Matplotlib not installed.") from exc + + +__all__ = ["plot_field", "plot_vec_field"] + + +# plotting routines ####################################################### + + +def plot_field( + fld, field="field", fig=None, ax=None, **kwargs +): # pragma: no cover + """ + Plot a spatial field. + + Parameters + ---------- + fld : :class:`Field` + The given Field class instance. + field : :class:`str`, optional + Field that should be plotted. Default: "field" + fig : :class:`Figure` or :any:`None`, optional + Figure to plot the axes on. If `None`, a new one will be created. + Default: `None` + ax : :class:`Axes` or :any:`None`, optional + Axes to plot on. If `None`, a new one will be added to the figure. + Default: `None` + **kwargs + Forwarded to the plotting routine. + """ + if fld.dim == 1: + return plot_1d(fld.pos, fld[field], fig, ax, fld.temporal, **kwargs) + return plot_nd( + fld.pos, + fld[field], + fld.mesh_type, + fig, + ax, + fld.latlon, + fld.temporal, + **kwargs, + ) + + +def plot_1d( + pos, field, fig=None, ax=None, temporal=False, ax_names=None +): # pragma: no cover + """ + Plot a 1D field. + + Parameters + ---------- + pos : :class:`list` + the position tuple, containing either the point coordinates (x, y, ...) + or the axes descriptions (for mesh_type='structured') + field : :class:`numpy.ndarray` + Field values. + temporal : :class:`bool`, optional + Indicate a metric spatio-temporal covariance model. + The time-dimension is assumed to be appended, + meaning the pos tuple is (x,y,z,...,t) or (lat, lon, t). + Default: False + fig : :class:`Figure` or :any:`None`, optional + Figure to plot the axes on. If `None`, a new one will be created. + Default: `None` + ax : :class:`Axes` or :any:`None`, optional + Axes to plot on. If `None`, a new one will be added to the figure. + Default: `None` + ax_names : :class:`list` of :class:`str`, optional + Axes names. The default is ["$x$", "field"]. + + Returns + ------- + ax : :class:`Axes` + Axis containing the plot. + """ + fig, ax = get_fig_ax(fig, ax) + title = f"Field 1D: {field.shape}" + x = pos[0] + x = x.flatten() + arg = np.argsort(x) + ax_names = _ax_names(1, temporal=temporal, ax_names=ax_names) + ax.plot(x[arg], field.ravel()[arg]) + ax.set_xlabel(ax_names[0]) + ax.set_ylabel(ax_names[1]) + ax.set_title(title) + fig.show() + return ax + + +def plot_nd( + pos, + field, + mesh_type, + fig=None, + ax=None, + latlon=False, + temporal=False, + resolution=128, + ax_names=None, + aspect="quad", + show_colorbar=True, + convex_hull=False, + contour_plot=True, + **kwargs, +): # pragma: no cover + """ + Plot field in arbitrary dimensions. + + Parameters + ---------- + pos : :class:`list` + the position tuple, containing either the point coordinates (x, y, ...) + or the axes descriptions (for mesh_type='structured') + field : :class:`numpy.ndarray` + Field values. + fig : :class:`Figure` or :any:`None`, optional + Figure to plot the axes on. If `None`, a new one will be created. + Default: `None` + ax : :class:`Axes` or :any:`None`, optional + Axes to plot on. If `None`, a new one will be added to the figure. + Default: `None` + latlon : :class:`bool`, optional + Whether the data is representing 2D fields on earths surface described + by latitude and longitude. When using this, the estimator will + use great-circle distance for variogram estimation. + Note, that only an isotropic variogram can be estimated and a + ValueError will be raised, if a direction was specified. + Bin edges need to be given in radians in this case. + Default: False + temporal : :class:`bool`, optional + Indicate a metric spatio-temporal covariance model. + The time-dimension is assumed to be appended, + meaning the pos tuple is (x,y,z,...,t) or (lat, lon, t). + Default: False + resolution : :class:`int`, optional + Resolution of the imshow plot. The default is 128. + ax_names : :class:`list` of :class:`str`, optional + Axes names. The default is ["$x$", "field"]. + aspect : :class:`str` or :any:`None` or :class:`float`, optional + Aspect of the plot. Can be "auto", "equal", "quad", None or a number + describing the aspect ratio. + The default is "quad". + show_colorbar : :class:`bool`, optional + Whether to show the colorbar. The default is True. + convex_hull : :class:`bool`, optional + Whether to show the convex hull in 2D with unstructured data. + The default is False. + contour_plot : :class:`bool`, optional + Whether to use a contour-plot in 2D. The default is True. + + Returns + ------- + ax : :class:`Axes` + Axis containing the plot. + """ + dim = len(pos) + assert dim > 1 + assert not latlon or dim == 2 + int(bool(temporal)) + if dim == 2 and contour_plot: + return _plot_2d( + pos, + field, + mesh_type, + fig, + ax, + latlon, + temporal, + ax_names, + **kwargs, + ) + if latlon: + # swap lat-lon to lon-lat (x-y) + if temporal: + pos = (pos[1], pos[0], pos[2]) + else: + pos = (pos[1], pos[0]) + if mesh_type != "unstructured": + field = np.moveaxis(field, [0, 1], [1, 0]) + ax_names = _ax_names(dim, latlon, temporal, ax_names) + # init planes + planes = rotation_planes(dim) + plane_names = [f" {ax_names[p[0]]} - {ax_names[p[1]]}" for p in planes] + ax_ends = [[p.min(), p.max()] for p in pos] + ax_rngs = [end[1] - end[0] for end in ax_ends] + ax_steps = [rng / resolution for rng in ax_rngs] + ax_extents = [ax_ends[p[0]] + ax_ends[p[1]] for p in planes] + # create figure + reformat = fig is None and ax is None + fig, ax = get_fig_ax(fig, ax) + ax.set_title(f"Field {dim}D {mesh_type} {field.shape}") + if reformat: # only format fig if it was created here + fig.set_size_inches(8, 5.5 + 0.5 * (dim - 2)) + # init additional axis, radio-buttons and sliders + s_frac = 0.5 * (dim - 2) / (6 + 0.5 * (dim - 2)) + s_size = s_frac / max(dim - 2, 1) + left, bottom = (0.25, s_frac + 0.13) if dim > 2 else (None, None) + fig.subplots_adjust(left=left, bottom=bottom) + slider = [] + for i in range(dim - 2, 0, -1): + slider_ax = fig.add_axes([0.3, i * s_size, 0.435, s_size * 0.6]) + slider.append(Slider(slider_ax, "", 0, 1, facecolor="grey")) + slider[-1].vline.set_color("k") + # create radio buttons + if dim > 2: + rax = fig.add_axes( + [0.05, 0.85 - 2 * s_frac, 0.15, 2 * s_frac], frame_on=0, alpha=0 + ) + rax.set_title(" Plane", loc="left") + radio = RadioButtons(rax, plane_names, activecolor="grey") + elif mesh_type == "unstructured" and convex_hull: + # show convex hull in 2D + hull = ConvexHull(pos.T) + for simplex in hull.simplices: + ax.plot(pos[0, simplex], pos[1, simplex], "k") + # init imshow and colorbar axis + grid = np.mgrid[0 : 1 : resolution * 1j, 0 : 1 : resolution * 1j] + f_ini, vmin, vmax = np.full_like(grid[0], np.nan), field.min(), field.max() + im = ax.imshow( + f_ini.T, interpolation="bicubic", origin="lower", vmin=vmin, vmax=vmax + ) + + # actions + def inter_plane(cuts, axes): + """Interpolate plane.""" + plane_ax = [] + for i, (rng, end, cut) in enumerate(zip(ax_rngs, ax_ends, cuts)): + if i in axes: + plane_ax.append(grid[axes.index(i)] * rng + end[0]) + else: + plane_ax.append(np.full_like(grid[0], cut, dtype=float)) + # needs to be a tuple + plane_ax = tuple(plane_ax) + if mesh_type != "unstructured": + return inter.interpn(pos, field, plane_ax, bounds_error=False) + return inter.griddata(pos.T, field, plane_ax, method="nearest") + + def update_field(*args): + """Sliders update.""" + p = plane_names.index(radio.value_selected) if dim > 2 else 0 + # dummy cut values for selected plane-axes (setting to 0) + cuts = [s.val for s in slider] + cuts.insert(planes[p][0], 0) + cuts.insert(planes[p][1], 0) + im.set_array(inter_plane(cuts, planes[p]).T) + fig.canvas.draw_idle() + + def update_plane(label): + """Radio button update.""" + p = plane_names.index(label) + cut_select = [i for i in range(dim) if i not in planes[p]] + # reset sliders + for i, s in zip(cut_select, slider): + s.label.set_text(ax_names[i]) + s.valmin, s.valmax = ax_ends[i] + s.valinit = ax_ends[i][0] + ax_rngs[i] / 2.0 + s.valstep = ax_steps[i] + s.ax.set_xlim(*ax_ends[i]) + # update representation + s.vline.set_data(2 * [s.valinit], [-0.1, 1.1]) + s.reset() + im.set_extent(ax_extents[p]) + asp = 1.0 # init value + if aspect == "quad": + asp = ax_rngs[planes[p][0]] / ax_rngs[planes[p][1]] + if aspect is not None: + ax.set_aspect(asp if aspect == "quad" else aspect) + ax.set_xlabel(ax_names[planes[p][0]]) + ax.set_ylabel(ax_names[planes[p][1]]) + update_field() + + # initial plot on xy plane + update_plane(plane_names[0]) + # bind actions + if dim > 2: + radio.on_clicked(update_plane) + for s in slider: + s.on_changed(update_field) + if show_colorbar: + fig.colorbar(im, ax=ax) + fig.show() + return ax + + +def plot_vec_field(fld, field="field", fig=None, ax=None): # pragma: no cover + """ + Plot a spatial random vector field. + + Parameters + ---------- + fld : :class:`Field` + The given field class instance. + field : :class:`str`, optional + Field that should be plotted. Default: "field" + fig : :class:`Figure` or :any:`None`, optional + Figure to plot the axes on. If `None`, a new one will be created. + Default: `None` + ax : :class:`Axes` or :any:`None`, optional + Axes to plot on. If `None`, a new one will be added to the figure. + Default: `None` + """ + if fld.mesh_type == "unstructured": + raise RuntimeError( + "Only structured vector fields are supported " + "for plotting. Please create one on a structured grid." + ) + plt_fld = fld[field] + norm = np.sqrt(plt_fld[0, :].T ** 2 + plt_fld[1, :].T ** 2) + + fig, ax = get_fig_ax(fig, ax) + title = f"Field 2D {fld.mesh_type}: {plt_fld.shape}" + x = fld.pos[0] + y = fld.pos[1] + + sp = plt.streamplot( + x, + y, + plt_fld[0, :].T, + plt_fld[1, :].T, + color=norm, + linewidth=norm / 2, + ) + ax.set_xlabel("X") + ax.set_ylabel("Y") + ax.set_title(title) + fig.colorbar(sp.lines) + fig.show() + return ax + + +def _ax_names(dim, latlon=False, temporal=False, ax_names=None): + t_fac = int(bool(temporal)) + if ax_names is not None: + assert len(ax_names) >= dim + return ax_names[:dim] + if dim == 2 + t_fac and latlon: + return ["lon", "lat"] + t_fac * ["time"] + if dim - t_fac <= 3: + return ( + ["$x$", "$y$", "$z$"][: dim - t_fac] + + t_fac * ["time"] + + (dim == 1) * ["field"] + ) + return [f"$x_{{{i}}}$" for i in range(dim - t_fac)] + t_fac * ["time"] + + +def _plot_2d( + pos, + field, + mesh_type, + fig=None, + ax=None, + latlon=False, + temporal=False, + ax_names=None, + levels=64, + antialias=True, +): # pragma: no cover + """Plot a 2d field with a contour plot.""" + fig, ax = get_fig_ax(fig, ax) + title = f"Field 2D {mesh_type}: {field.shape}" + ax_names = _ax_names(2, latlon, temporal, ax_names=ax_names) + x, y = pos[::-1] if latlon else pos + if mesh_type == "unstructured": + cont = ax.tricontourf(x, y, field.ravel(), levels=levels) + if antialias: + ax.tricontour(x, y, field.ravel(), levels=levels, zorder=-10) + else: + plt_fld = field if latlon else field.T + cont = ax.contourf(x, y, plt_fld, levels=levels) + if antialias: + ax.contour(x, y, plt_fld, levels=levels, zorder=-10) + ax.set_xlabel(ax_names[0]) + ax.set_ylabel(ax_names[1]) + ax.set_title(title) + fig.colorbar(cont) + fig.show() + return ax diff --git a/GSTools/source/src/gstools/field/srf.py b/GSTools/source/src/gstools/field/srf.py new file mode 100644 index 0000000000000000000000000000000000000000..0646251d9c3ac3596e23d001f1c6f9e26677b483 --- /dev/null +++ b/GSTools/source/src/gstools/field/srf.py @@ -0,0 +1,223 @@ +""" +GStools subpackage providing a class for standard spatial random fields. + +.. currentmodule:: gstools.field.srf + +The following classes are provided + +.. autosummary:: + SRF +""" + +import numpy as np + +from gstools.field.base import Field +from gstools.field.generator import ( + Fourier, + Generator, + IncomprRandMeth, + RandMeth, +) +from gstools.field.upscaling import var_coarse_graining, var_no_scaling + +__all__ = ["SRF"] + +GENERATOR = { + "RandMeth": RandMeth, + "IncomprRandMeth": IncomprRandMeth, + "VectorField": IncomprRandMeth, + "VelocityField": IncomprRandMeth, + "Fourier": Fourier, +} +"""dict: Standard generators for spatial random fields.""" + +UPSCALING = { + "coarse_graining": var_coarse_graining, + "no_scaling": var_no_scaling, +} +"""dict: Upscaling routines for spatial random fields.""" + + +class SRF(Field): + """A class to generate spatial random fields (SRF). + + Parameters + ---------- + model : :any:`CovModel` + Covariance Model of the spatial random field. + mean : :class:`float` or :any:`callable`, optional + Mean of the SRF (in normal form). Could also be a callable. + The default is 0.0. + normalizer : :any:`None` or :any:`Normalizer`, optional + Normalizer to be applied to the SRF to transform the field values. + The default is None. + trend : :any:`None` or :class:`float` or :any:`callable`, optional + Trend of the SRF (in transformed form). + If no normalizer is applied, this behaves equal to 'mean'. + The default is None. + upscaling : :class:`str`, optional + Method to be used for upscaling the variance at each point + depending on the related element volume. + See the ``point_volumes`` keyword in the :any:`SRF.__call__` routine. + At the moment, the following upscaling methods are provided: + + * "no_scaling" : No upscaling is applied to the variance. + See: :any:`var_no_scaling` + * "coarse_graining" : A volume depended variance is + calculated by the upscaling technique coarse graining. + See: :any:`var_coarse_graining` + + Default: "no_scaling" + generator : :class:`str` or :any:`Generator`, optional + Name or class of the field generator to be used. + At the moment, the following generators are provided: + + * "RandMeth" : The Randomization Method. + See: :any:`RandMeth` + * "IncomprRandMeth" : The incompressible Randomization Method. + This is the original algorithm proposed by Kraichnan 1970 + See: :any:`IncomprRandMeth` + * "VectorField" : an alias for "IncomprRandMeth" + * "VelocityField" : an alias for "IncomprRandMeth" + * "Fourier" : the periodic Fourier method + + Default: "RandMeth" + **generator_kwargs + Keyword arguments that are forwarded to the generator in use. + Have a look at the provided generators for further information. + """ + + def __init__( + self, + model, + mean=0.0, + normalizer=None, + trend=None, + upscaling="no_scaling", + generator="RandMeth", + **generator_kwargs, + ): + super().__init__(model, mean=mean, normalizer=normalizer, trend=trend) + # initialize private attributes + self._generator = None + self._upscaling = None + self._upscaling_func = None + # initialize attributes + self.upscaling = upscaling + self.set_generator(generator, **generator_kwargs) + + def __call__( + self, + pos=None, + seed=np.nan, + point_volumes=0.0, + mesh_type="unstructured", + post_process=True, + store=True, + ): + """Generate the spatial random field. + + The field is saved as `self.field` and is also returned. + + Parameters + ---------- + pos : :class:`list`, optional + the position tuple, containing main direction and transversal + directions + seed : :class:`int`, optional + seed for RNG for resetting. Default: keep seed from generator + point_volumes : :class:`float` or :class:`numpy.ndarray` + If your evaluation points for the field are coming from a mesh, + they are probably representing a certain element volume. + This volume can be passed by `point_volumes` to apply the + given variance upscaling. If `point_volumes` is ``0`` nothing + is changed. Default: ``0`` + mesh_type : :class:`str` + 'structured' / 'unstructured' + post_process : :class:`bool`, optional + Whether to apply mean, normalizer and trend to the field. + Default: `True` + store : :class:`str` or :class:`bool`, optional + Whether to store field (True/False) with default name + or with specified name. + The default is :any:`True` for default name "field". + + Returns + ------- + field : :class:`numpy.ndarray` + the SRF + """ + name, save = self.get_store_config(store) + # update the model/seed in the generator if any changes were made + self.generator.update(self.model, seed) + # get isometrized positions and the resulting field-shape + iso_pos, shape = self.pre_pos(pos, mesh_type) + # generate the field + field = np.reshape(self.generator(iso_pos), shape) + # upscaled variance + if not np.isscalar(point_volumes) or not np.isclose(point_volumes, 0): + scaled_var = self.upscaling_func(self.model, point_volumes) + if np.size(scaled_var) > 1: + scaled_var = np.reshape(scaled_var, shape) + field *= np.sqrt(scaled_var / self.model.sill) + return self.post_field(field, name, post_process, save) + + def upscaling_func(self, *args, **kwargs): + """Upscaling method applied to the field variance.""" + return self._upscaling_func(*args, **kwargs) + + def set_generator(self, generator, **generator_kwargs): + """Set the generator for the field. + + Parameters + ---------- + generator : :class:`str` or :any:`Generator`, optional + Name or class of the field generator to be used. + Default: "RandMeth" + **generator_kwargs + keyword arguments that are forwarded to the generator in use. + """ + gen = GENERATOR[generator] if generator in GENERATOR else generator + if not (isinstance(gen, type) and issubclass(gen, Generator)): + raise ValueError( + f"gstools.SRF: Unknown or wrong generator: {generator}" + ) + self._generator = gen(self.model, **generator_kwargs) + self.value_type = self.generator.value_type + + for val in [self.mean, self.trend]: + if not callable(val) and val is not None: + if np.size(val) > 1 and self.value_type == "scalar": + raise ValueError(f"Mean/Trend: Wrong size ({val})") + + @property + def generator(self): + """:any:`callable`: The generator of the field. + + Default: :any:`RandMeth` + """ + return self._generator + + @property + def upscaling(self): # pragma: no cover + """:class:`str`: Name of the upscaling method. + + See the ``point_volumes`` keyword in the :any:`SRF.__call__` routine. + Default: "no_scaling" + """ + return self._upscaling + + @upscaling.setter + def upscaling(self, upscaling): + if upscaling in UPSCALING: + self._upscaling = upscaling + self._upscaling_func = UPSCALING[upscaling] + else: + raise ValueError(f"SRF: Unknown upscaling method: {upscaling}") + + def __repr__(self): + """Return String representation.""" + return ( + f"{self.name}(model={self.model.name}" + f"{self._fmt_mean_norm_trend()}, generator={self.generator.name})" + ) diff --git a/GSTools/source/src/gstools/field/tools.py b/GSTools/source/src/gstools/field/tools.py new file mode 100644 index 0000000000000000000000000000000000000000..0f528f5edd31258e5d466731e359c316477c43b4 --- /dev/null +++ b/GSTools/source/src/gstools/field/tools.py @@ -0,0 +1,256 @@ +""" +GStools subpackage providing tools for Fields. + +.. currentmodule:: gstools.field.tools + +The following classes and functions are provided + +.. autosummary:: + fmt_mean_norm_trend + to_vtk_helper + generate_on_mesh +""" + +import meshio +import numpy as np + +from gstools.normalizer import Normalizer +from gstools.tools.export import to_vtk, vtk_export +from gstools.tools.misc import list_format + +__all__ = ["fmt_mean_norm_trend", "to_vtk_helper", "generate_on_mesh"] + + +def _fmt_func_val(f_cls, func_val): # pragma: no cover + if func_val is None: + return str(None) + if callable(func_val): + return "" # or format(func_val.__name__) + if np.size(func_val) > 1: + return list_format(func_val, prec=f_cls.model._prec) + return f"{float(func_val):.{f_cls.model._prec}}" + + +def _fmt_normalizer(f_cls): # pragma: no cover + norm = f_cls.normalizer + return str(None) if norm.__class__ is Normalizer else norm.name + + +def fmt_mean_norm_trend(f_cls): # pragma: no cover + """Format string repr. for mean, normalizer and trend of a field.""" + args = [ + "mean=" + _fmt_func_val(f_cls, f_cls.mean), + "normalizer=" + _fmt_normalizer(f_cls), + "trend=" + _fmt_func_val(f_cls, f_cls.trend), + ] + return "".join([", " + arg for arg in args if not arg.endswith("None")]) + + +def to_vtk_helper( + f_cls, filename=None, field_select="field", fieldname="field" +): # pragma: no cover + """Create a VTK/PyVista grid of the field or save it as a VTK file. + + This is an internal helper that will handle saving or creating objects + + Parameters + ---------- + f_cls : :any:`Field` + Field class in use. + filename : :class:`str` + Filename of the file to be saved, including the path. Note that an + ending (.vtr or .vtu) will be added to the name. If ``None`` is + passed, a PyVista dataset of the appropriate type will be returned. + field_select : :class:`str`, optional + Field that should be stored. Can be: + "field", "raw_field", "krige_field", "err_field" or "krige_var". + Default: "field" + fieldname : :class:`str`, optional + Name of the field in the VTK file. Default: "field" + """ + field = f_cls[field_select] if field_select in f_cls.field_names else None + if f_cls.value_type == "vector": + if not (f_cls.pos is None or field is None or f_cls.mesh_type is None): + suf = ["_X", "_Y", "_Z"] + fields = {} + for i in range(f_cls.model.dim): + fields[fieldname + suf[i]] = field[i] + if filename is None: + return to_vtk(f_cls.pos, fields, f_cls.mesh_type) + return vtk_export(filename, f_cls.pos, fields, f_cls.mesh_type) + raise ValueError(f"Field.to_vtk: '{field_select}' not available.") + if f_cls.value_type == "scalar": + if not (f_cls.pos is None or field is None or f_cls.mesh_type is None): + if filename is None: + return to_vtk(f_cls.pos, {fieldname: field}, f_cls.mesh_type) + return vtk_export( + filename, f_cls.pos, {fieldname: field}, f_cls.mesh_type + ) + raise ValueError(f"Field.to_vtk: '{field_select}' not available.") + raise ValueError(f"Unknown field value type: {f_cls.value_type}") + + +def generate_on_mesh( + f_cls, mesh, points="centroids", direction="all", name="field", **kwargs +): + """Generate a field on a given meshio, ogs5py or pyvista mesh. + + Parameters + ---------- + f_cls : :any:`Field` + The field class in use. + mesh : meshio.Mesh or ogs5py.MSH or PyVista mesh + The given meshio, ogs5py, or PyVista mesh + points : :class:`str`, optional + The points to evaluate the field at. + Either the "centroids" of the mesh cells + (calculated as mean of the cell vertices) or the "points" + of the given mesh. + Default: "centroids" + direction : :class:`str` or :class:`list`, optional + Here you can state which direction should be chosen for + lower dimension. For example, if you got a 2D mesh in xz direction, + you have to pass "xz". By default, all directions are used. + One can also pass a list of indices. + Default: "all" + name : :class:`str` or :class:`list` of :class:`str`, optional + Name(s) to store the field(s) in the given mesh as point_data or + cell_data. If to few names are given, digits will be appended. + Default: "field" + **kwargs + Keyword arguments forwarded to `Field.__call__`. + + Notes + ----- + This will store the field in the given mesh under the given name, + if a meshio or PyVista mesh was given. + + See: https://github.com/nschloe/meshio + + See: https://github.com/GeoStat-Framework/ogs5py + + See: https://github.com/pyvista/pyvista + """ + has_pyvista = False + has_ogs5py = False + + try: + import pyvista as pv + + has_pyvista = True + except ImportError: + pass + try: + import ogs5py as ogs + + has_ogs5py = True + except ImportError: + pass + + if isinstance(direction, str) and direction == "all": + select = list(range(f_cls.dim)) + elif isinstance(direction, str): + select = _get_select(direction)[: f_cls.dim] + else: + select = direction[: f_cls.dim] + if len(select) < f_cls.dim: + raise ValueError( + f"Field.mesh: need at least {f_cls.dim} direction(s), " + f"got '{direction}'" + ) + # convert pyvista mesh + if has_pyvista and pv.is_pyvista_dataset(mesh): + if points == "centroids": + pnts = mesh.cell_centers().points.T[select] + else: + pnts = mesh.points.T[select] + out = f_cls.unstructured(pos=pnts, **kwargs) + # Deal with the output + fields = [out] if isinstance(out, np.ndarray) else out + if f_cls.value_type == "vector": + fields = [f.T for f in fields] + for f_name, field in zip(_names(name, len(fields)), fields): + mesh[f_name] = field + # convert ogs5py mesh + elif has_ogs5py and isinstance(mesh, ogs.MSH): + if points == "centroids": + pnts = mesh.centroids_flat.T[select] + else: + pnts = mesh.NODES.T[select] + out = f_cls.unstructured(pos=pnts, **kwargs) + # convert meshio mesh + elif isinstance(mesh, meshio.Mesh): + if points == "centroids": + # define unique order of cells + offset = [] + length = [] + mesh_dim = mesh.points.shape[1] + if mesh_dim < f_cls.dim: + raise ValueError("Field.mesh: mesh dimension too low!") + pnts = np.empty((0, mesh_dim), dtype=np.double) + for cell in mesh.cells: + pnt = np.mean(mesh.points[cell.data], axis=1) + offset.append(pnts.shape[0]) + length.append(pnt.shape[0]) + pnts = np.vstack((pnts, pnt)) + # generate pos for __call__ + pnts = pnts.T[select] + out = f_cls.unstructured(pos=pnts, **kwargs) + fields = [out] if isinstance(out, np.ndarray) else out + if f_cls.value_type == "vector": + fields = [f.T for f in fields] + f_lists = [] + for field in fields: + f_list = [] + for off, leng in zip(offset, length): + f_list.append(field[off : off + leng]) + f_lists.append(f_list) + for f_name, f_list in zip(_names(name, len(f_lists)), f_lists): + mesh.cell_data[f_name] = f_list + else: + out = f_cls.unstructured(pos=mesh.points.T[select], **kwargs) + fields = [out] if isinstance(out, np.ndarray) else out + if f_cls.value_type == "vector": + fields = [f.T for f in fields] + for f_name, field in zip(_names(name, len(fields)), fields): + mesh.point_data[f_name] = field + else: + raise ValueError("Field.mesh: Unknown mesh format!") + return out + + +def _names(name, cnt): + name = [name] if isinstance(name, str) else list(name)[:cnt] + if len(name) < cnt: + name += [f"{name[-1]}{i + 1}" for i in range(cnt - len(name))] + return name + + +def _get_select(direction): + select = [] + if not 0 < len(direction) < 4: + raise ValueError( + f"Field.mesh: need 1 to 3 direction(s), got '{direction}'" + ) + for axis in direction: + if axis == "x": + if 0 in select: + raise ValueError( + f"Field.mesh: got duplicate directions {direction}" + ) + select.append(0) + elif axis == "y": + if 1 in select: + raise ValueError( + f"Field.mesh: got duplicate directions {direction}" + ) + select.append(1) + elif axis == "z": + if 2 in select: + raise ValueError( + f"Field.mesh: got duplicate directions {direction}" + ) + select.append(2) + else: + raise ValueError(f"Field.mesh: got unknown direction {axis}") + return select diff --git a/GSTools/source/src/gstools/field/upscaling.py b/GSTools/source/src/gstools/field/upscaling.py new file mode 100644 index 0000000000000000000000000000000000000000..1cb24c4664a2358c58adeb8fdf10e813554a22a5 --- /dev/null +++ b/GSTools/source/src/gstools/field/upscaling.py @@ -0,0 +1,97 @@ +""" +GStools subpackage providing upscaling routines for the spatial random field. + +.. currentmodule:: gstools.field.upscaling + +The following functions are provided + +.. autosummary:: + :toctree: + + var_coarse_graining + var_no_scaling +""" + +import warnings + +import numpy as np + +__all__ = ["var_coarse_graining", "var_no_scaling"] + + +# scaling routines ############################################################ + + +def var_coarse_graining(model, point_volumes=0.0): + r"""Coarse Graning procedure to upscale the variance for uniform flow. + + Parameters + ---------- + model : :any:`CovModel` + Covariance Model used for the field. + point_volumes : :class:`float` or :class:`numpy.ndarray` + Volumes of the elements at the given points. Default: ``0`` + + Returns + ------- + scaled_var : :class:`float` or :class:`numpy.ndarray` + The upscaled variance + + Notes + ----- + This procedure was presented in [Attinger03]_. It applies the + upscaling procedure 'Coarse Graining' to the Groundwater flow equation + under uniform flow on a lognormal distributed conductivity field following + a gaussian covariance function. A filter over a cube with a given + edge-length :math:`\lambda` is applied and an upscaled conductivity field + is obtained. + The upscaled field is again following a gaussian covariance function with + scale dependent variance and length-scale: + + .. math:: + \lambda &= V^{\frac{1}{d}} \\ + \sigma^2\left(\lambda\right) &= + \sigma^2\cdot\left( + \frac{\ell^2}{\ell^2+\left(\frac{\lambda}{2}\right)^2} + \right)^{\frac{d}{2}} \\ + \ell\left(\lambda\right) &= + \left(\ell^2+\left(\frac{\lambda}{2}\right)^2\right)^{\frac{1}{2}} + + Therby :math:`\lambda` will be calculated from the given + ``point_volumes`` :math:`V` by assuming a cube with the given volume. + + The upscaled length scale will be ignored by this routine. + + References + ---------- + .. [Attinger03] Attinger, S. 2003, + ''Generalized coarse graining procedures for flow in porous media'', + Computational Geosciences, 7(4), 253–273. + """ + if not np.isclose(model.nugget, 0): + warnings.warn( + "var_coarse_graining: non-zero nugget will violate upscaling!" + ) + # interpret volume as a hypercube and calculate the edge length + edge = point_volumes ** (1.0 / model.dim) + var_factor = (model.len_scale**2 / (model.len_scale**2 + edge**2 / 4)) ** ( + model.dim / 2.0 + ) + + return model.sill * var_factor + + +def var_no_scaling(model, *args, **kwargs): + r"""Dummy function to bypass scaling. + + Parameters + ---------- + model : :any:`CovModel` + Covariance Model used for the field. + + Returns + ------- + var : :class:`float` + The model variance. + """ + return model.sill diff --git a/GSTools/source/src/gstools/krige/__init__.py b/GSTools/source/src/gstools/krige/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..66d032464b38e45998b6fb4eedd217f67abb9511 --- /dev/null +++ b/GSTools/source/src/gstools/krige/__init__.py @@ -0,0 +1,29 @@ +""" +GStools subpackage providing kriging. + +.. currentmodule:: gstools.krige + +Kriging Classes +^^^^^^^^^^^^^^^ + +.. autosummary:: + :toctree: + + Krige + Simple + Ordinary + Universal + ExtDrift + Detrended +""" + +from gstools.krige.base import Krige +from gstools.krige.methods import ( + Detrended, + ExtDrift, + Ordinary, + Simple, + Universal, +) + +__all__ = ["Krige", "Simple", "Ordinary", "Universal", "ExtDrift", "Detrended"] diff --git a/GSTools/source/src/gstools/krige/base.py b/GSTools/source/src/gstools/krige/base.py new file mode 100644 index 0000000000000000000000000000000000000000..a1887434d7bd28fc64fc6269ad1f1b152c77e073 --- /dev/null +++ b/GSTools/source/src/gstools/krige/base.py @@ -0,0 +1,753 @@ +""" +GStools subpackage providing a base class for kriging. + +.. currentmodule:: gstools.krige.base + +The following classes are provided + +.. autosummary:: + Krige +""" + +import collections + +import numpy as np +import scipy.linalg as spl +from gstools_cython.krige import calc_field_krige as calc_field_krige_c +from gstools_cython.krige import ( + calc_field_krige_and_variance as calc_field_krige_and_variance_c, +) +from scipy.spatial.distance import cdist + +from gstools import config +from gstools.field.base import Field +from gstools.krige.tools import get_drift_functions, set_condition +from gstools.tools.geometric import rotated_main_axes +from gstools.tools.misc import eval_func +from gstools.variogram import vario_estimate + +if config._GSTOOLS_CORE_AVAIL: # pragma: no cover + from gstools_core import calc_field_krige as calc_field_krige_gsc + from gstools_core import ( + calc_field_krige_and_variance as calc_field_krige_and_variance_gsc, + ) + +__all__ = ["Krige"] + + +P_INV = {"pinv": spl.pinv, "pinvh": spl.pinvh} +"""dict: Standard pseudo-inverse routines""" + + +def _calc_field_krige(krig_mat, krig_vecs, cond, num_threads=None): + """A wrapper function for calling the krige algorithms.""" + if config.USE_GSTOOLS_CORE and config._GSTOOLS_CORE_AVAIL: + calc_field_krige_fct = calc_field_krige_gsc + else: + calc_field_krige_fct = calc_field_krige_c + return calc_field_krige_fct(krig_mat, krig_vecs, cond, num_threads) + + +def _calc_field_krige_and_variance( + krig_mat, krig_vecs, cond, num_threads=None +): + """A wrapper function for calling the krige algorithms.""" + if config.USE_GSTOOLS_CORE and config._GSTOOLS_CORE_AVAIL: + calc_field_krige_and_variance_fct = calc_field_krige_and_variance_gsc + else: + calc_field_krige_and_variance_fct = calc_field_krige_and_variance_c + return calc_field_krige_and_variance_fct( + krig_mat, krig_vecs, cond, num_threads + ) + + +class Krige(Field): + """ + A Swiss Army knife for kriging. + + A Kriging class enabling the basic kriging routines: + Simple-, Ordinary-, Universal-, External Drift- + and detrended/regression-Kriging as well as + Kriging the Mean [Wackernagel2003]_. + + Parameters + ---------- + model : :any:`CovModel` + Covariance Model used for kriging. + cond_pos : :class:`list` + tuple, containing the given condition positions (x, [y, z]) + cond_val : :class:`numpy.ndarray` + the values of the conditions (nan values will be ignored) + drift_functions : :class:`list` of :any:`callable`, :class:`str` or :class:`int` + Either a list of callable functions, an integer representing + the polynomial order of the drift or one of the following strings: + + * "linear" : regional linear drift (equals order=1) + * "quadratic" : regional quadratic drift (equals order=2) + + ext_drift : :class:`numpy.ndarray` or :any:`None`, optional + the external drift values at the given cond. positions. + mean : :class:`float`, optional + mean value used to shift normalized conditioning data. + Could also be a callable. The default is None. + normalizer : :any:`None` or :any:`Normalizer`, optional + Normalizer to be applied to the input data to gain normality. + The default is None. + trend : :any:`None` or :class:`float` or :any:`callable`, optional + A callable trend function. Should have the signature: f(x, [y, z, ...]) + This is used for detrended kriging, where the trended is subtracted + from the conditions before kriging is applied. + This can be used for regression kriging, where the trend function + is determined by an external regression algorithm. + If no normalizer is applied, this behaves equal to 'mean'. + The default is None. + unbiased : :class:`bool`, optional + Whether the kriging weights should sum up to 1, so the estimator + is unbiased. If unbiased is `False` and no drifts are given, + this results in simple kriging. + Default: True + exact : :class:`bool`, optional + Whether the interpolator should reproduce the exact input values. + If `False`, `cond_err` is interpreted as measurement error + at the conditioning points and the result will be more smooth. + Default: False + cond_err : :class:`str`, :class :class:`float` or :class:`list`, optional + The measurement error at the conditioning points. + Either "nugget" to apply the model-nugget, a single value applied to + all points or an array with individual values for each point. + The "exact=True" variant only works with "cond_err='nugget'". + Default: "nugget" + pseudo_inv : :class:`bool`, optional + Whether the kriging system is solved with the pseudo inverted + kriging matrix. If `True`, this leads to more numerical stability + and redundant points are averaged. But it can take more time. + Default: True + pseudo_inv_type : :class:`str` or :any:`callable`, optional + Here you can select the algorithm to compute the pseudo-inverse matrix: + + * `"pinv"`: use `pinv` from `scipy` which uses `SVD` + * `"pinvh"`: use `pinvh` from `scipy` which uses eigen-values + + If you want to use another routine to invert the kriging matrix, + you can pass a callable which takes a matrix and returns the inverse. + Default: `"pinv"` + fit_normalizer : :class:`bool`, optional + Whether to fit the data-normalizer to the given conditioning data. + Default: False + fit_variogram : :class:`bool`, optional + Whether to fit the given variogram model to the data. + Directional variogram fitting is triggered by setting + any anisotropy factor of the model to anything unequal 1 + but the main axes of correlation are taken from the model + rotation angles. If the model is a spatio-temporal latlon + model, this will raise an error. + This assumes the sill to be the data variance and with + standard bins provided by the :any:`standard_bins` routine. + Default: False + + Notes + ----- + If you have changed any properties in the class, you can update the kriging + setup by calling :any:`Krige.set_condition` without any arguments. + + References + ---------- + .. [Wackernagel2003] Wackernagel, H., + "Multivariate geostatistics", + Springer, Berlin, Heidelberg (2003) + """ + + valid_value_types = ["scalar"] + """:class:`list` of :class:`str`: valid field value types.""" + + default_field_names = ["field", "krige_var", "mean_field"] + """:class:`list`: Default field names.""" + + def __init__( + self, + model, + cond_pos, + cond_val, + drift_functions=None, + ext_drift=None, + mean=None, + normalizer=None, + trend=None, + unbiased=True, + exact=False, + cond_err="nugget", + pseudo_inv=True, + pseudo_inv_type="pinv", + fit_normalizer=False, + fit_variogram=False, + ): + super().__init__(model, mean=mean, normalizer=normalizer, trend=trend) + self._unbiased = bool(unbiased) + self._exact = bool(exact) + self._pseudo_inv = bool(pseudo_inv) + self._pseudo_inv_type = None + self.pseudo_inv_type = pseudo_inv_type + # initialize private attributes + self._cond_pos = None + self._cond_val = None + self._cond_err = None + self._krige_mat = None + self._krige_pos = None + self._cond_trend = None + self._cond_ext_drift = np.array([]) + self._drift_functions = None + self.set_drift_functions(drift_functions) + self.set_condition( + cond_pos, + cond_val, + ext_drift, + cond_err, + fit_normalizer, + fit_variogram, + ) + + def __call__( + self, + pos=None, + mesh_type="unstructured", + ext_drift=None, + chunk_size=None, + only_mean=False, + return_var=True, + post_process=True, + store=True, + ): + """ + Generate the kriging field. + + The field is saved as `self.field` and is also returned. + The error variance is saved as `self.krige_var` and is also returned. + + Parameters + ---------- + pos : :class:`list`, optional + the position tuple, containing main direction and transversal + directions (x, [y, z]) + mesh_type : :class:`str`, optional + 'structured' / 'unstructured' + ext_drift : :class:`numpy.ndarray` or :any:`None`, optional + the external drift values at the given positions (only for EDK) + chunk_size : :class:`int`, optional + Chunk size to cut down the size of the kriging system to prevent + memory errors. + Default: None + only_mean : :class:`bool`, optional + Whether to only calculate the mean of the kriging field. + Default: `False` + return_var : :class:`bool`, optional + Whether to return the variance along with the field. + Default: `True` + post_process : :class:`bool`, optional + Whether to apply mean, normalizer and trend to the field. + Default: `True` + store : :class:`str` or :class:`bool` or :class:`list`, optional + Whether to store kriging fields (True/False) with default name + or with specified names. + The default is :any:`True` for default names + ["field", "krige_var"] or "mean_field" if `only_mean=True`. + + Returns + ------- + field : :class:`numpy.ndarray` + the kriged field or mean_field + krige_var : :class:`numpy.ndarray`, optional + the kriging error variance + (if return_var is True and only_mean is False) + """ + return_var &= not only_mean # don't return variance when calc. mean + fld_cnt = 2 if return_var else 1 + default = self.default_field_names[2] if only_mean else None + name, save = self.get_store_config(store, default, fld_cnt) + + iso_pos, shape = self.pre_pos(pos, mesh_type) + pnt_cnt = len(iso_pos[0]) + + field = np.empty(pnt_cnt, dtype=np.double) + krige_var = np.empty(pnt_cnt, dtype=np.double) if return_var else None + # set constant mean if present and wanted + if only_mean and self.drift_no == 0: + field[...] = self.get_mean(post_process=False) + # execute the kriging routine + else: + # set chunk size + chunk_size = pnt_cnt if chunk_size is None else int(chunk_size) + chunk_no = int(np.ceil(pnt_cnt / chunk_size)) + ext_drift = self._pre_ext_drift(pnt_cnt, ext_drift) + # iterate chunks + for i in range(chunk_no): + # get chunk slice for actual chunk + chunk_slice = ( + i * chunk_size, + min(pnt_cnt, (i + 1) * chunk_size), + ) + c_slice = slice(*chunk_slice) + # get RHS of the kriging system + k_vec = self._get_krige_vecs( + iso_pos, chunk_slice, ext_drift, only_mean + ) + # generate the raw kriging field and error variance + self._summate(field, krige_var, c_slice, k_vec, return_var) + # reshape field if we got a structured mesh + field = np.reshape(field, shape) + # save field to class + field = self.post_field(field, name[0], post_process, save[0]) + if return_var: # care about the estimated error variance + krige_var = np.reshape( + np.maximum(self.model.sill - krige_var, 0), shape + ) + krige_var = self.post_field(krige_var, name[1], False, save[1]) + return field, krige_var + return field + + def _summate(self, field, krige_var, c_slice, k_vec, return_var): + if return_var: # estimate error variance + field[c_slice], krige_var[c_slice] = ( + _calc_field_krige_and_variance( + self._krige_mat, k_vec, self._krige_cond + ) + ) + else: # solely calculate the interpolated field + field[c_slice] = _calc_field_krige( + self._krige_mat, k_vec, self._krige_cond + ) + + def _inv(self, mat): + # return pseudo-inverted matrix if wanted (numerically more stable) + if self.pseudo_inv: + # if the given type is a callable, call it + if callable(self.pseudo_inv_type): + return self.pseudo_inv_type(mat) + # use the selected method to compute the pseudo-inverse matrix + return P_INV[self.pseudo_inv_type](mat) + # if no pseudo-inverse is wanted, calculate the real inverse + return spl.inv(mat) + + def _get_krige_mat(self): + """Calculate the inverse matrix of the kriging equation.""" + res = np.empty((self.krige_size, self.krige_size), dtype=np.double) + # fill the kriging matrix with the covariance + res[: self.cond_no, : self.cond_no] = self.model.covariance( + self._get_dists(self._krige_pos) + ) + # apply the measurement error (nugget by default) + res[np.diag_indices(self.cond_no)] += self.cond_err + # set unbias condition (weights have to sum up to 1) + if self.unbiased: + res[self.cond_no, : self.cond_no] = 1 + res[: self.cond_no, self.cond_no] = 1 + # set functional drift terms + for i, f in enumerate(self.drift_functions): + drift_tmp = f(*self.cond_pos) + res[-self.drift_no + i, : self.cond_no] = drift_tmp + res[: self.cond_no, -self.drift_no + i] = drift_tmp + # set external drift terms + if self.ext_drift_no > 0: + ext_size = self.krige_size - self.ext_drift_no + res[ext_size:, : self.cond_no] = self.cond_ext_drift + res[: self.cond_no, ext_size:] = self.cond_ext_drift.T + # set lower right part of the matrix to 0 + res[self.cond_no :, self.cond_no :] = 0 + return self._inv(res) + + def _get_krige_vecs( + self, pos, chunk_slice=(0, None), ext_drift=None, only_mean=False + ): + """Calculate the RHS of the kriging equation.""" + # determine the chunk size + chunk_size = len(pos[0]) if chunk_slice[1] is None else chunk_slice[1] + chunk_size -= chunk_slice[0] + chunk_pos = None # init value + res = np.empty((self.krige_size, chunk_size), dtype=np.double) + if only_mean: + # set points to limit of the covariance to only get the mean + res[: self.cond_no, :] = 0 + else: + # get correct covariance functions (depending on exact values) + cf = self.model.cov_nugget if self.exact else self.model.covariance + res[: self.cond_no, :] = cf( + self._get_dists(self._krige_pos, pos, chunk_slice) + ) + # apply the unbiased condition + if self.unbiased: + res[self.cond_no, :] = 1 + # drift function need the anisotropic and rotated positions + if self.int_drift_no > 0: + chunk_pos = self.model.anisometrize(pos)[:, slice(*chunk_slice)] + # apply functional drift + for i, f in enumerate(self.drift_functions): + res[-self.drift_no + i, :] = f(*chunk_pos) + # apply external drift + if self.ext_drift_no > 0: + ext_size = self.krige_size - self.ext_drift_no + res[ext_size:, :] = ext_drift[:, slice(*chunk_slice)] + return res + + def _pre_ext_drift(self, pnt_cnt, ext_drift=None, set_cond=False): + """ + Preprocessor for external drifts. + + Parameters + ---------- + pnt_cnt : :class:`numpy.ndarray` + Number of points of the mesh. + ext_drift : :class:`numpy.ndarray` or :any:`None`, optional + the external drift values at the given positions (only for EDK) + For multiple external drifts, the first dimension + should be the index of the drift term. + set_cond : :class:`bool`, optional + State if the given external drift is set for the conditioning + points. Default: False + + Returns + ------- + ext_drift : :class:`numpy.ndarray` or :any:`None` + the drift values at the given positions + """ + if ext_drift is not None: + ext_drift = np.atleast_2d(np.asarray(ext_drift, dtype=np.double)) + if ext_drift.size == 0: # treat empty array as no ext_drift + return np.array([]) + if set_cond: + if len(ext_drift.shape) > 2 or ext_drift.shape[1] != pnt_cnt: + raise ValueError("Krige: wrong number of ext. drifts.") + return ext_drift + ext_shape = np.shape(ext_drift) + shape = (self.ext_drift_no, pnt_cnt) + if self.drift_no > 1 and ext_shape[0] != self.ext_drift_no: + raise ValueError("Krige: wrong number of external drifts.") + if np.prod(ext_shape) != np.prod(shape): + raise ValueError("Krige: wrong number of ext. drift values.") + return np.asarray(ext_drift, dtype=np.double).reshape(shape) + if not set_cond and self._cond_ext_drift.size > 0: + raise ValueError("Krige: wrong number of ext. drift values.") + return np.array([]) + + def _get_dists(self, pos1, pos2=None, pos2_slice=(0, None)): + """ + Calculate pairwise distances. + + Parameters + ---------- + pos1 : :class:`tuple` of :class:`numpy.ndarray` + the first position tuple + pos2 : :class:`tuple` of :class:`numpy.ndarray`, optional + the second position tuple. If none, the first one is taken. + pos2_slice : :class:`tuple` of :class:`int`, optional + Start and stop of slice for the pos2 array. Default: all values. + + Returns + ------- + :class:`numpy.ndarray` + Matrix containing the pairwise distances. + """ + if pos2 is None: + return cdist(pos1.T, pos1.T) + return cdist(pos1.T, pos2.T[slice(*pos2_slice), ...]) + + def get_mean(self, post_process=True): + """Calculate the estimated mean of the detrended field. + + Parameters + ---------- + post_process : :class:`bool`, optional + Whether to apply field-mean and normalizer. + Default: `True` + + Returns + ------- + mean : :class:`float` or :any:`None` + Mean of the Kriging System. + + Notes + ----- + Only not ``None`` if the Kriging System has a constant mean. + This means, no drift is given and the given field-mean is constant. + The result is neglecting a potential given trend. + """ + # if there are drift-terms, no constant mean can be calculated -> None + # if mean should not be post-processed, it exists when no drift given + if not self.has_const_mean and (post_process or self.drift_no > 0): + return None + res = 0.0 # for simple kriging return the given mean + # correctly setting given mean + mean = 0.0 if self.mean is None else self.mean + # for ordinary kriging return the estimated mean + if self.unbiased: + # set the right side of the kriging system to the limit of cov. + mean_est = np.concatenate((np.full_like(self.cond_val, 0.0), [1])) + # execute the kriging routine with einsum + res = np.einsum( + "i,ij,j", self._krige_cond, self._krige_mat, mean_est + ) + return self.normalizer.denormalize(res + mean) if post_process else res + + def set_condition( + self, + cond_pos=None, + cond_val=None, + ext_drift=None, + cond_err=None, + fit_normalizer=False, + fit_variogram=False, + ): + """Set the conditions for kriging. + + This method could also be used to update the kriging setup, when + properties were changed. Then you can call it without arguments. + + Parameters + ---------- + cond_pos : :class:`list`, optional + the position tuple of the conditions (x, [y, z]). Default: current. + cond_val : :class:`numpy.ndarray`, optional + the values of the conditions (nan values will be ignored). + Default: current. + ext_drift : :class:`numpy.ndarray` or :any:`None`, optional + the external drift values at the given conditions (only for EDK) + For multiple external drifts, the first dimension + should be the index of the drift term. When passing `None`, the + extisting external drift will be used. + cond_err : :class:`str`, :class :class:`float`, :class:`list`, optional + The measurement error at the conditioning points. + Either "nugget" to apply the model-nugget, a single value applied + to all points or an array with individual values for each point. + The measurement error has to be <= nugget. + The "exact=True" variant only works with "cond_err='nugget'". + Default: "nugget" + fit_normalizer : :class:`bool`, optional + Whether to fit the data-normalizer to the given conditioning data. + Default: False + fit_variogram : :class:`bool`, optional + Whether to fit the given variogram model to the data. + Directional variogram fitting is triggered by setting + any anisotropy factor of the model to anything unequal 1 + but the main axes of correlation are taken from the model + rotation angles. If the model is a spatio-temporal latlon + model, this will raise an error. + This assumes the sill to be the data variance and with + standard bins provided by the :any:`standard_bins` routine. + Default: False + """ + # only use existing external drift, if no new positions are given + ext_drift = ( + self._cond_ext_drift + if (ext_drift is None and cond_pos is None) + else ext_drift + ) + # use existing values or set default + cond_pos = self._cond_pos if cond_pos is None else cond_pos + cond_val = self._cond_val if cond_val is None else cond_val + cond_err = self._cond_err if cond_err is None else cond_err + cond_err = "nugget" if cond_err is None else cond_err # default + if cond_pos is None or cond_val is None: + raise ValueError("Krige.set_condition: missing cond_pos/cond_val.") + # correctly format cond_pos and cond_val + self._cond_pos, self._cond_val = set_condition( + cond_pos, cond_val, self.dim + ) + if fit_normalizer: # fit normalizer to detrended data + self.normalizer.fit(self.cond_val - self.cond_trend) + if fit_variogram: # fitting model to empirical variogram of data + # normalize field + if self.model.latlon and self.model.temporal: + msg = "Krige: can't fit variogram for spatio-temporal latlon data." + raise ValueError(msg) + field = self.normalizer.normalize(self.cond_val - self.cond_trend) + field -= self.cond_mean + sill = np.var(field) + if self.model.is_isotropic: + emp_vario = vario_estimate( + self.cond_pos, + field, + latlon=self.model.latlon, + geo_scale=self.model.geo_scale, + ) + else: + axes = rotated_main_axes(self.model.dim, self.model.angles) + emp_vario = vario_estimate( + self.cond_pos, field, direction=axes + ) + # set the sill to the field variance + self.model.fit_variogram(*emp_vario, sill=sill) + # set the measurement errors + self.cond_err = cond_err + # set the external drift values and the conditioning points + self._cond_ext_drift = self._pre_ext_drift( + self.cond_no, ext_drift, set_cond=True + ) + # upate the internal kriging settings + self._krige_pos = self.model.isometrize(self.cond_pos) + # krige pos are the unrotated and isotropic condition positions + self._krige_mat = self._get_krige_mat() + + def set_drift_functions(self, drift_functions=None): + """ + Set the drift functions for universal kriging. + + Parameters + ---------- + drift_functions : :class:`list` of :any:`callable`, :class:`str` or :class:`int` + Either a list of callable functions, an integer representing + the polynomial order of the drift or one of the following strings: + + * "linear" : regional linear drift (equals order=1) + * "quadratic" : regional quadratic drift (equals order=2) + + Raises + ------ + ValueError + If the given drift functions are not callable. + """ + if drift_functions is None: + self._drift_functions = [] + elif isinstance(drift_functions, (str, int)): + self._drift_functions = get_drift_functions( + self.dim, drift_functions + ) + else: + if isinstance(drift_functions, collections.abc.Iterator): + drift_functions = list(drift_functions) + # check for a single content thats not a string + try: + iter(drift_functions) + except TypeError: + drift_functions = [drift_functions] + for f in drift_functions: + if not callable(f): + raise ValueError("Krige: Drift functions not callable") + self._drift_functions = drift_functions + + @property + def _krige_cond(self): + """:class:`numpy.ndarray`: The prepared kriging conditions.""" + pad_size = self.drift_no + int(self.unbiased) + # detrend data and normalize + val = self.normalizer.normalize(self.cond_val - self.cond_trend) + # set to zero mean + val -= self.cond_mean + return np.pad(val, (0, pad_size), mode="constant", constant_values=0) + + @property + def cond_pos(self): + """:class:`list`: The position tuple of the conditions.""" + return self._cond_pos + + @property + def cond_val(self): + """:class:`list`: The values of the conditions.""" + return self._cond_val + + @property + def cond_err(self): + """:class:`list`: The measurement errors at the condition points.""" + if isinstance(self._cond_err, str) and self._cond_err == "nugget": + return self.model.nugget + return self._cond_err + + @cond_err.setter + def cond_err(self, value): + if isinstance(value, str) and value == "nugget": + self._cond_err = value + else: + if self.exact: + raise ValueError( + "krige.cond_err: measurement errors can't be given, " + "when interpolator should be exact." + ) + value = np.asarray(value, dtype=np.double).reshape(-1) + if value.size == 1: + self._cond_err = value.item() + else: + if value.size != self.cond_no: + raise ValueError( + "krige.cond_err: wrong number of measurement errors." + ) + self._cond_err = value + + @property + def cond_no(self): + """:class:`int`: The number of the conditions.""" + return len(self._cond_val) + + @property + def cond_ext_drift(self): + """:class:`numpy.ndarray`: The ext. drift at the conditions.""" + return self._cond_ext_drift + + @property + def cond_mean(self): + """:class:`numpy.ndarray`: Trend at the conditions.""" + return eval_func(self.mean, self.cond_pos, self.dim, broadcast=True) + + @property + def cond_trend(self): + """:class:`numpy.ndarray`: Trend at the conditions.""" + return eval_func(self.trend, self.cond_pos, self.dim, broadcast=True) + + @property + def unbiased(self): + """:class:`bool`: Whether the kriging is unbiased or not.""" + return self._unbiased + + @property + def exact(self): + """:class:`bool`: Whether the interpolator is exact.""" + return self._exact + + @property + def pseudo_inv(self): + """:class:`bool`: Whether pseudo inverse matrix is used.""" + return self._pseudo_inv + + @property + def pseudo_inv_type(self): + """:class:`str`: Method selector for pseudo inverse calculation.""" + return self._pseudo_inv_type + + @pseudo_inv_type.setter + def pseudo_inv_type(self, val): + if val not in P_INV and not callable(val): + raise ValueError(f"Krige: pseudo_inv_type not in {sorted(P_INV)}") + self._pseudo_inv_type = val + + @property + def drift_functions(self): + """:class:`list` of :any:`callable`: The drift functions.""" + return self._drift_functions + + @property + def has_const_mean(self): + """:class:`bool`: Whether the field has a constant mean or not.""" + return self.drift_no == 0 and not callable(self.mean) + + @property + def krige_size(self): + """:class:`int`: Size of the kriging system.""" + return self.cond_no + self.drift_no + int(self.unbiased) + + @property + def drift_no(self): + """:class:`int`: Number of drift values per point.""" + return self.int_drift_no + self.ext_drift_no + + @property + def int_drift_no(self): + """:class:`int`: Number of internal drift values per point.""" + return len(self.drift_functions) + + @property + def ext_drift_no(self): + """:class:`int`: Number of external drift values per point.""" + return self.cond_ext_drift.shape[0] + + def __repr__(self): + """Return String representation.""" + return ( + f"{self.name}(model={self.model.name}, " + f"cond_no={self.cond_no}{self._fmt_mean_norm_trend()})" + ) diff --git a/GSTools/source/src/gstools/krige/methods.py b/GSTools/source/src/gstools/krige/methods.py new file mode 100644 index 0000000000000000000000000000000000000000..e1cbffa99ea9cb88d8b3faaee09a5ecf7fb87199 --- /dev/null +++ b/GSTools/source/src/gstools/krige/methods.py @@ -0,0 +1,519 @@ +""" +GStools subpackage providing a class for simple kriging. + +.. currentmodule:: gstools.krige.methods + +The following classes are provided + +.. autosummary:: + Simple + Ordinary + Universal + ExtDrift + Detrended +""" + +from gstools.krige.base import Krige + +__all__ = ["Simple", "Ordinary", "Universal", "ExtDrift", "Detrended"] + + +class Simple(Krige): + """ + Simple kriging. + + Simple kriging is used to interpolate data with a given mean. + + Parameters + ---------- + model : :any:`CovModel` + Covariance Model used for kriging. + cond_pos : :class:`list` + tuple, containing the given condition positions (x, [y, z]) + cond_val : :class:`numpy.ndarray` + the values of the conditions (nan values will be ignored) + mean : :class:`float`, optional + mean value used to shift normalized conditioning data. + Could also be a callable. The default is None. + normalizer : :any:`None` or :any:`Normalizer`, optional + Normalizer to be applied to the input data to gain normality. + The default is None. + trend : :any:`None` or :class:`float` or :any:`callable`, optional + A callable trend function. Should have the signature: f(x, [y, z, ...]) + This is used for detrended kriging, where the trended is subtracted + from the conditions before kriging is applied. + This can be used for regression kriging, where the trend function + is determined by an external regression algorithm. + If no normalizer is applied, this behaves equal to 'mean'. + The default is None. + exact : :class:`bool`, optional + Whether the interpolator should reproduce the exact input values. + If `False`, `cond_err` is interpreted as measurement error + at the conditioning points and the result will be more smooth. + Default: False + cond_err : :class:`str`, :class :class:`float` or :class:`list`, optional + The measurement error at the conditioning points. + Either "nugget" to apply the model-nugget, a single value applied to + all points or an array with individual values for each point. + The measurement error has to be <= nugget. + The "exact=True" variant only works with "cond_err='nugget'". + Default: "nugget" + pseudo_inv : :class:`bool`, optional + Whether the kriging system is solved with the pseudo inverted + kriging matrix. If `True`, this leads to more numerical stability + and redundant points are averaged. But it can take more time. + Default: True + pseudo_inv_type : :class:`str` or :any:`callable`, optional + Here you can select the algorithm to compute the pseudo-inverse matrix: + + * `"pinv"`: use `pinv` from `scipy` which uses `SVD` + * `"pinvh"`: use `pinvh` from `scipy` which uses eigen-values + + If you want to use another routine to invert the kriging matrix, + you can pass a callable which takes a matrix and returns the inverse. + Default: `"pinv"` + fit_normalizer : :class:`bool`, optional + Whether to fit the data-normalizer to the given conditioning data. + Default: False + fit_variogram : :class:`bool`, optional + Whether to fit the given variogram model to the data. + Directional variogram fitting is triggered by setting + any anisotropy factor of the model to anything unequal 1 + but the main axes of correlation are taken from the model + rotation angles. If the model is a spatio-temporal latlon + model, this will raise an error. + This assumes the sill to be the data variance and with + standard bins provided by the :any:`standard_bins` routine. + Default: False + """ + + def __init__( + self, + model, + cond_pos, + cond_val, + mean=0.0, + normalizer=None, + trend=None, + exact=False, + cond_err="nugget", + pseudo_inv=True, + pseudo_inv_type="pinv", + fit_normalizer=False, + fit_variogram=False, + ): + super().__init__( + model, + cond_pos, + cond_val, + mean=mean, + normalizer=normalizer, + trend=trend, + unbiased=False, + exact=exact, + cond_err=cond_err, + pseudo_inv=pseudo_inv, + pseudo_inv_type=pseudo_inv_type, + fit_normalizer=fit_normalizer, + fit_variogram=fit_variogram, + ) + + +class Ordinary(Krige): + """ + Ordinary kriging. + + Ordinary kriging is used to interpolate data and estimate a proper mean. + + Parameters + ---------- + model : :any:`CovModel` + Covariance Model used for kriging. + cond_pos : :class:`list` + tuple, containing the given condition positions (x, [y, z]) + cond_val : :class:`numpy.ndarray` + the values of the conditions (nan values will be ignored) + normalizer : :any:`None` or :any:`Normalizer`, optional + Normalizer to be applied to the input data to gain normality. + The default is None. + trend : :any:`None` or :class:`float` or :any:`callable`, optional + A callable trend function. Should have the signature: f(x, [y, z, ...]) + This is used for detrended kriging, where the trended is subtracted + from the conditions before kriging is applied. + This can be used for regression kriging, where the trend function + is determined by an external regression algorithm. + If no normalizer is applied, this behaves equal to 'mean'. + The default is None. + exact : :class:`bool`, optional + Whether the interpolator should reproduce the exact input values. + If `False`, `cond_err` is interpreted as measurement error + at the conditioning points and the result will be more smooth. + Default: False + cond_err : :class:`str`, :class :class:`float` or :class:`list`, optional + The measurement error at the conditioning points. + Either "nugget" to apply the model-nugget, a single value applied to + all points or an array with individual values for each point. + The measurement error has to be <= nugget. + The "exact=True" variant only works with "cond_err='nugget'". + Default: "nugget" + pseudo_inv : :class:`bool`, optional + Whether the kriging system is solved with the pseudo inverted + kriging matrix. If `True`, this leads to more numerical stability + and redundant points are averaged. But it can take more time. + Default: True + pseudo_inv_type : :class:`str` or :any:`callable`, optional + Here you can select the algorithm to compute the pseudo-inverse matrix: + + * `"pinv"`: use `pinv` from `scipy` which uses `SVD` + * `"pinvh"`: use `pinvh` from `scipy` which uses eigen-values + + If you want to use another routine to invert the kriging matrix, + you can pass a callable which takes a matrix and returns the inverse. + Default: `"pinv"` + fit_normalizer : :class:`bool`, optional + Whether to fit the data-normalizer to the given conditioning data. + Default: False + fit_variogram : :class:`bool`, optional + Whether to fit the given variogram model to the data. + Directional variogram fitting is triggered by setting + any anisotropy factor of the model to anything unequal 1 + but the main axes of correlation are taken from the model + rotation angles. If the model is a spatio-temporal latlon + model, this will raise an error. + This assumes the sill to be the data variance and with + standard bins provided by the :any:`standard_bins` routine. + Default: False + """ + + def __init__( + self, + model, + cond_pos, + cond_val, + normalizer=None, + trend=None, + exact=False, + cond_err="nugget", + pseudo_inv=True, + pseudo_inv_type="pinv", + fit_normalizer=False, + fit_variogram=False, + ): + super().__init__( + model, + cond_pos, + cond_val, + trend=trend, + normalizer=normalizer, + exact=exact, + cond_err=cond_err, + pseudo_inv=pseudo_inv, + pseudo_inv_type=pseudo_inv_type, + fit_normalizer=fit_normalizer, + fit_variogram=fit_variogram, + ) + + +class Universal(Krige): + """ + Universal kriging. + + Universal kriging is used to interpolate given data with a variable mean, + that is determined by a functional drift. + + This estimator is set to be unbiased by default. + This means, that the weights in the kriging equation sum up to 1. + Consequently no constant function needs to be given for a constant drift, + since the unbiased condition is applied to all given drift functions. + + Parameters + ---------- + model : :any:`CovModel` + Covariance Model used for kriging. + cond_pos : :class:`list` + tuple, containing the given condition positions (x, [y, z]) + cond_val : :class:`numpy.ndarray` + the values of the conditions (nan values will be ignored) + drift_functions : :class:`list` of :any:`callable`, :class:`str` or :class:`int` + Either a list of callable functions, an integer representing + the polynomial order of the drift or one of the following strings: + + * "linear" : regional linear drift (equals order=1) + * "quadratic" : regional quadratic drift (equals order=2) + + normalizer : :any:`None` or :any:`Normalizer`, optional + Normalizer to be applied to the input data to gain normality. + The default is None. + trend : :any:`None` or :class:`float` or :any:`callable`, optional + A callable trend function. Should have the signature: f(x, [y, z, ...]) + This is used for detrended kriging, where the trended is subtracted + from the conditions before kriging is applied. + This can be used for regression kriging, where the trend function + is determined by an external regression algorithm. + If no normalizer is applied, this behaves equal to 'mean'. + The default is None. + exact : :class:`bool`, optional + Whether the interpolator should reproduce the exact input values. + If `False`, `cond_err` is interpreted as measurement error + at the conditioning points and the result will be more smooth. + Default: False + cond_err : :class:`str`, :class :class:`float` or :class:`list`, optional + The measurement error at the conditioning points. + Either "nugget" to apply the model-nugget, a single value applied to + all points or an array with individual values for each point. + The measurement error has to be <= nugget. + The "exact=True" variant only works with "cond_err='nugget'". + Default: "nugget" + pseudo_inv : :class:`bool`, optional + Whether the kriging system is solved with the pseudo inverted + kriging matrix. If `True`, this leads to more numerical stability + and redundant points are averaged. But it can take more time. + Default: True + pseudo_inv_type : :class:`str` or :any:`callable`, optional + Here you can select the algorithm to compute the pseudo-inverse matrix: + + * `"pinv"`: use `pinv` from `scipy` which uses `SVD` + * `"pinvh"`: use `pinvh` from `scipy` which uses eigen-values + + If you want to use another routine to invert the kriging matrix, + you can pass a callable which takes a matrix and returns the inverse. + Default: `"pinv"` + fit_normalizer : :class:`bool`, optional + Whether to fit the data-normalizer to the given conditioning data. + Default: False + fit_variogram : :class:`bool`, optional + Whether to fit the given variogram model to the data. + Directional variogram fitting is triggered by setting + any anisotropy factor of the model to anything unequal 1 + but the main axes of correlation are taken from the model + rotation angles. If the model is a spatio-temporal latlon + model, this will raise an error. + This assumes the sill to be the data variance and with + standard bins provided by the :any:`standard_bins` routine. + Default: False + """ + + def __init__( + self, + model, + cond_pos, + cond_val, + drift_functions, + normalizer=None, + trend=None, + exact=False, + cond_err="nugget", + pseudo_inv=True, + pseudo_inv_type="pinv", + fit_normalizer=False, + fit_variogram=False, + ): + super().__init__( + model, + cond_pos, + cond_val, + drift_functions=drift_functions, + normalizer=normalizer, + trend=trend, + exact=exact, + cond_err=cond_err, + pseudo_inv=pseudo_inv, + pseudo_inv_type=pseudo_inv_type, + fit_normalizer=fit_normalizer, + fit_variogram=fit_variogram, + ) + + +class ExtDrift(Krige): + """ + External drift kriging (EDK). + + External drift kriging is used to interpolate given data + with a variable mean, that is determined by an external drift. + + This estimator is set to be unbiased by default. + This means, that the weights in the kriging equation sum up to 1. + Consequently no constant external drift needs to be given to estimate + a proper mean. + + Parameters + ---------- + model : :any:`CovModel` + Covariance Model used for kriging. + cond_pos : :class:`list` + tuple, containing the given condition positions (x, [y, z]) + cond_val : :class:`numpy.ndarray` + the values of the conditions (nan values will be ignored) + ext_drift : :class:`numpy.ndarray` + the external drift values at the given condition positions. + normalizer : :any:`None` or :any:`Normalizer`, optional + Normalizer to be applied to the input data to gain normality. + The default is None. + trend : :any:`None` or :class:`float` or :any:`callable`, optional + A callable trend function. Should have the signature: f(x, [y, z, ...]) + This is used for detrended kriging, where the trended is subtracted + from the conditions before kriging is applied. + This can be used for regression kriging, where the trend function + is determined by an external regression algorithm. + If no normalizer is applied, this behaves equal to 'mean'. + The default is None. + exact : :class:`bool`, optional + Whether the interpolator should reproduce the exact input values. + If `False`, `cond_err` is interpreted as measurement error + at the conditioning points and the result will be more smooth. + Default: False + cond_err : :class:`str`, :class :class:`float` or :class:`list`, optional + The measurement error at the conditioning points. + Either "nugget" to apply the model-nugget, a single value applied to + all points or an array with individual values for each point. + The measurement error has to be <= nugget. + The "exact=True" variant only works with "cond_err='nugget'". + Default: "nugget" + pseudo_inv : :class:`bool`, optional + Whether the kriging system is solved with the pseudo inverted + kriging matrix. If `True`, this leads to more numerical stability + and redundant points are averaged. But it can take more time. + Default: True + pseudo_inv_type : :class:`str` or :any:`callable`, optional + Here you can select the algorithm to compute the pseudo-inverse matrix: + + * `"pinv"`: use `pinv` from `scipy` which uses `SVD` + * `"pinvh"`: use `pinvh` from `scipy` which uses eigen-values + + If you want to use another routine to invert the kriging matrix, + you can pass a callable which takes a matrix and returns the inverse. + Default: `"pinv"` + fit_normalizer : :class:`bool`, optional + Whether to fit the data-normalizer to the given conditioning data. + Default: False + fit_variogram : :class:`bool`, optional + Whether to fit the given variogram model to the data. + Directional variogram fitting is triggered by setting + any anisotropy factor of the model to anything unequal 1 + but the main axes of correlation are taken from the model + rotation angles. If the model is a spatio-temporal latlon + model, this will raise an error. + This assumes the sill to be the data variance and with + standard bins provided by the :any:`standard_bins` routine. + Default: False + """ + + def __init__( + self, + model, + cond_pos, + cond_val, + ext_drift, + normalizer=None, + trend=None, + exact=False, + cond_err="nugget", + pseudo_inv=True, + pseudo_inv_type="pinv", + fit_normalizer=False, + fit_variogram=False, + ): + super().__init__( + model, + cond_pos, + cond_val, + ext_drift=ext_drift, + normalizer=normalizer, + trend=trend, + exact=exact, + cond_err=cond_err, + pseudo_inv=pseudo_inv, + pseudo_inv_type=pseudo_inv_type, + fit_normalizer=fit_normalizer, + fit_variogram=fit_variogram, + ) + + +class Detrended(Krige): + """ + Detrended simple kriging. + + In detrended kriging, the data is detrended before interpolation by + simple kriging with zero mean. + + The trend needs to be a callable function the user has to provide. + This can be used for regression kriging, where the trend function + is determined by an external regression algorithm. + + This is just a shortcut for simple kriging with a given trend function, + zero mean and no normalizer. + + A trend can be given with EVERY provided kriging routine. + + Parameters + ---------- + model : :any:`CovModel` + Covariance Model used for kriging. + cond_pos : :class:`list` + tuple, containing the given condition positions (x, [y, z]) + cond_val : :class:`numpy.ndarray` + the values of the conditions (nan values will be ignored) + trend_function : :any:`callable` + The callable trend function. Should have the signature: f(x, [y, z]) + exact : :class:`bool`, optional + Whether the interpolator should reproduce the exact input values. + If `False`, `cond_err` is interpreted as measurement error + at the conditioning points and the result will be more smooth. + Default: False + cond_err : :class:`str`, :class :class:`float` or :class:`list`, optional + The measurement error at the conditioning points. + Either "nugget" to apply the model-nugget, a single value applied to + all points or an array with individual values for each point. + The measurement error has to be <= nugget. + The "exact=True" variant only works with "cond_err='nugget'". + Default: "nugget" + pseudo_inv : :class:`bool`, optional + Whether the kriging system is solved with the pseudo inverted + kriging matrix. If `True`, this leads to more numerical stability + and redundant points are averaged. But it can take more time. + Default: True + pseudo_inv_type : :class:`str` or :any:`callable`, optional + Here you can select the algorithm to compute the pseudo-inverse matrix: + + * `"pinv"`: use `pinv` from `scipy` which uses `SVD` + * `"pinvh"`: use `pinvh` from `scipy` which uses eigen-values + + If you want to use another routine to invert the kriging matrix, + you can pass a callable which takes a matrix and returns the inverse. + Default: `"pinv"` + fit_variogram : :class:`bool`, optional + Whether to fit the given variogram model to the data. + Directional variogram fitting is triggered by setting + any anisotropy factor of the model to anything unequal 1 + but the main axes of correlation are taken from the model + rotation angles. If the model is a spatio-temporal latlon + model, this will raise an error. + This assumes the sill to be the data variance and with + standard bins provided by the :any:`standard_bins` routine. + Default: False + """ + + def __init__( + self, + model, + cond_pos, + cond_val, + trend, + exact=False, + cond_err="nugget", + pseudo_inv=True, + pseudo_inv_type="pinv", + fit_variogram=False, + ): + super().__init__( + model, + cond_pos, + cond_val, + trend=trend, + unbiased=False, + exact=exact, + cond_err=cond_err, + pseudo_inv=pseudo_inv, + pseudo_inv_type=pseudo_inv_type, + fit_variogram=fit_variogram, + ) diff --git a/GSTools/source/src/gstools/krige/tools.py b/GSTools/source/src/gstools/krige/tools.py new file mode 100644 index 0000000000000000000000000000000000000000..e6d13f97853eded9894cff3f8fcd776c4094e1b5 --- /dev/null +++ b/GSTools/source/src/gstools/krige/tools.py @@ -0,0 +1,95 @@ +""" +GStools subpackage providing tools for Kriging. + +.. currentmodule:: gstools.krige.tools + +The following classes and functions are provided + +.. autosummary:: + set_condition + get_drift_functions +""" + +from itertools import combinations_with_replacement + +import numpy as np + +__all__ = ["set_condition", "get_drift_functions"] + + +def set_condition(cond_pos, cond_val, dim): + """ + Set the conditions for kriging. + + Parameters + ---------- + cond_pos : :class:`list` + the position tuple of the conditions (x, [y, z]) + cond_val : :class:`numpy.ndarray` + the values of the conditions (nan values will be ignored) + dim : :class:`int`, optional + Spatial dimension + + Raises + ------ + ValueError + If the given data does not match the given dimension. + + Returns + ------- + cond_pos : :class:`list` + the error checked cond_pos with all finite values + cond_val : :class:`numpy.ndarray` + the error checked cond_val for all finite cond_pos values + """ + # convert the input for right shapes and dimension checks + cond_val = np.asarray(cond_val, dtype=np.double).reshape(-1) + cond_pos = np.asarray(cond_pos, dtype=np.double).reshape(dim, -1) + if len(cond_pos[0]) != len(cond_val): + raise ValueError( + "Please check your 'cond_pos' and 'cond_val' parameters. " + "The shapes do not match." + ) + mask = np.isfinite(cond_val) + return cond_pos[:, mask], cond_val[mask] + + +def get_drift_functions(dim, drift_type): + """ + Get functions for a given drift type in universal kriging. + + Parameters + ---------- + dim : :class:`int` + Given dimension. + drift_type : :class:`str` or :class:`int` + Drift type: 'linear' or 'quadratic' or an integer for the polynomial + order of the drift type. (linear equals 1, quadratic equals 2 ...) + + Returns + ------- + :class:`list` of :any:`callable` + List of drift functions. + """ + if drift_type in ["lin", "linear"]: + drift_type = 1 + elif drift_type in ["quad", "quadratic"]: + drift_type = 2 + else: + drift_type = int(drift_type) + drift_functions = [] + for d in range(drift_type): + selects = combinations_with_replacement(range(dim), d + 1) + for select in selects: + drift_functions.append(_f_factory(select)) + return drift_functions + + +def _f_factory(select): + def f(*pos): + res = 1.0 + for i in select: + res *= np.asarray(pos[i]) + return res + + return f diff --git a/GSTools/source/src/gstools/normalizer/__init__.py b/GSTools/source/src/gstools/normalizer/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..505a6d67fbb78c60c2ed9837cfbc57fdae96b2eb --- /dev/null +++ b/GSTools/source/src/gstools/normalizer/__init__.py @@ -0,0 +1,61 @@ +""" +GStools subpackage providing normalization routines. + +.. currentmodule:: gstools.normalizer + +Base-Normalizer +^^^^^^^^^^^^^^^ + +.. autosummary:: + :toctree: + + Normalizer + +Field-Normalizer +^^^^^^^^^^^^^^^^ + +.. autosummary:: + :toctree: + + LogNormal + BoxCox + BoxCoxShift + YeoJohnson + Modulus + Manly + +Convenience Routines +^^^^^^^^^^^^^^^^^^^^ + +.. autosummary:: + :toctree: + + apply_mean_norm_trend + remove_trend_norm_mean +""" + +from gstools.normalizer.base import Normalizer +from gstools.normalizer.methods import ( + BoxCox, + BoxCoxShift, + LogNormal, + Manly, + Modulus, + YeoJohnson, +) +from gstools.normalizer.tools import ( + apply_mean_norm_trend, + remove_trend_norm_mean, +) + +__all__ = [ + "Normalizer", + "LogNormal", + "BoxCox", + "BoxCoxShift", + "YeoJohnson", + "Modulus", + "Manly", + "apply_mean_norm_trend", + "remove_trend_norm_mean", +] diff --git a/GSTools/source/src/gstools/normalizer/base.py b/GSTools/source/src/gstools/normalizer/base.py new file mode 100644 index 0000000000000000000000000000000000000000..2aedd4d9503cd2e9ca6afb3858a8cb70181efef1 --- /dev/null +++ b/GSTools/source/src/gstools/normalizer/base.py @@ -0,0 +1,263 @@ +""" +GStools subpackage providing the base class for normalizers. + +.. currentmodule:: gstools.normalizer.base + +The following classes are provided + +.. autosummary:: + Normalizer +""" + +import warnings + +import numpy as np +import scipy.optimize as spo + + +def _derivative(f, x, dx=1e-6): + """Central difference formula.""" + return (f(x + dx) - f(x - dx)) / (2 * dx) + + +class Normalizer: + """Normalizer class. + + Parameters + ---------- + data : array_like, optional + Input data to fit the transformation to in order to gain normality. + The default is None. + **parameter + Specified parameters given by name. If not given, default parameters + will be used. + """ + + default_parameter = {} + """:class:`dict`: Default parameters of the Normalizer.""" + normalize_range = (-np.inf, np.inf) + """:class:`tuple`: Valid range for input data.""" + denormalize_range = (-np.inf, np.inf) + """:class:`tuple`: Valid range for output/normal data.""" + _dx = 1e-6 # dx for numerical derivative + + def __init__(self, data=None, **parameter): + # only use parameter, that have a provided default value + for key, value in self.default_parameter.items(): + setattr(self, key, parameter.get(key, value)) + # fit parameters if data is given + if data is not None: + self.fit(data) + # optimization results + self._opti = None + # precision for printing + self._prec = 3 + + def _denormalize(self, data): + return data + + def _normalize(self, data): + return data + + def _derivative(self, data): + return _derivative(self._normalize, data, dx=self._dx) + + def _loglikelihood(self, data): + add = -0.5 * np.size(data) * (np.log(2 * np.pi) + 1) + return self._kernel_loglikelihood(data) + add + + def _kernel_loglikelihood(self, data): + res = -0.5 * np.size(data) * np.log(np.var(self._normalize(data))) + return res + np.sum(np.log(np.maximum(1e-16, self._derivative(data)))) + + def _check_input(self, data, data_range=None, return_output_template=True): + is_data = np.logical_not(np.isnan(data)) + if return_output_template: + out = np.full_like(data, np.nan, dtype=np.double) + data = np.asarray(data, dtype=np.double)[is_data] + if data_range is not None and np.min(np.abs(data_range)) < np.inf: + dat_in = np.logical_and(data > data_range[0], data < data_range[1]) + if not np.all(dat_in): + warnings.warn( + f"{self.name}: " + f"data (min: {np.min(data)}, max: {np.max(data)}) " + f"out of range: {data_range}. " + "Affected values will be treated as NaN." + ) + is_data[is_data] &= dat_in + data = data[dat_in] + if return_output_template: + return data, is_data, out + return data + + def denormalize(self, data): + """Transform to input distribution. + + Parameters + ---------- + data : array_like + Input data (normal distributed). + + Returns + ------- + :class:`numpy.ndarray` + Denormalized data. + """ + data, is_data, out = self._check_input(data, self.denormalize_range) + out[is_data] = self._denormalize(data) + return out + + def normalize(self, data): + """Transform to normal distribution. + + Parameters + ---------- + data : array_like + Input data (not normal distributed). + + Returns + ------- + :class:`numpy.ndarray` + Normalized data. + """ + data, is_data, out = self._check_input(data, self.normalize_range) + out[is_data] = self._normalize(data) + return out + + def derivative(self, data): + """Factor for normal PDF to gain target PDF. + + Parameters + ---------- + data : array_like + Input data (not normal distributed). + + Returns + ------- + :class:`numpy.ndarray` + Derivative of the normalization transformation function. + """ + data, is_data, out = self._check_input(data, self.normalize_range) + out[is_data] = self._derivative(data) + return out + + def likelihood(self, data): + """Likelihood for given data with current parameters. + + Parameters + ---------- + data : array_like + Input data to fit the transformation to in order to gain normality. + + Returns + ------- + :class:`float` + Likelihood of the given data. + """ + return np.exp(self.loglikelihood(data)) + + def loglikelihood(self, data): + """Log-Likelihood for given data with current parameters. + + Parameters + ---------- + data : array_like + Input data to fit the transformation to in order to gain normality. + + Returns + ------- + :class:`float` + Log-Likelihood of the given data. + """ + data = self._check_input(data, self.normalize_range, False) + return self._loglikelihood(data) + + def kernel_loglikelihood(self, data): + """Kernel Log-Likelihood for given data with current parameters. + + Parameters + ---------- + data : array_like + Input data to fit the transformation to in order to gain normality. + + Returns + ------- + :class:`float` + Kernel Log-Likelihood of the given data. + + Notes + ----- + This loglikelihood function is neglecting additive constants, + that are not needed for optimization. + """ + data = self._check_input(data, self.normalize_range, False) + return self._kernel_loglikelihood(data) + + def fit(self, data, skip=None, **kwargs): + """Fitting the transformation to data by maximizing Log-Likelihood. + + Parameters + ---------- + data : array_like + Input data to fit the transformation to in order to gain normality. + skip : :class:`list` of :class:`str` or :any:`None`, optional + Names of parameters to be skipped in fitting. + The default is None. + **kwargs + Keyword arguments passed to :any:`scipy.optimize.minimize_scalar` + when only one parameter present or :any:`scipy.optimize.minimize`. + + Returns + ------- + :class:`dict` + Optimal parameters given by names. + """ + skip = [] if skip is None else skip + all_names = sorted(self.default_parameter) + para_names = [name for name in all_names if name not in skip] + + def _neg_kllf(par, dat): + for name, val in zip(para_names, np.atleast_1d(par)): + setattr(self, name, val) + return -self.kernel_loglikelihood(dat) + + if len(para_names) == 0: # transformations without para. (no opti.) + warnings.warn(f"{self.name}.fit: no parameters!") + return {} + if len(para_names) == 1: # one-para. transformations (simple opti.) + # default bracket like in scipy's boxcox (if not given) + kwargs.setdefault("bracket", (-2, 2)) + out = spo.minimize_scalar(_neg_kllf, args=(data,), **kwargs) + else: # general case + # init guess from current parameters (if x0 not given) + kwargs.setdefault("x0", [getattr(self, p) for p in para_names]) + out = spo.minimize(_neg_kllf, args=(data,), **kwargs) + # save optimization results + self._opti = out + for name, val in zip(para_names, np.atleast_1d(out.x)): + setattr(self, name, val) + return {name: getattr(self, name) for name in all_names} + + def __eq__(self, other): + """Compare Normalizers.""" + # check for correct base class + if type(self) is not type(other): + return False + # if base class is same, this is safe + for val in self.default_parameter: + if not np.isclose(getattr(self, val), getattr(other, val)): + return False + return True + + @property + def name(self): + """:class:`str`: The name of the normalizer class.""" + return self.__class__.__name__ + + def __repr__(self): + """Return String representation.""" + para_strs = [ + f"{p}={float(getattr(self, p)):.{self._prec}}" + for p in sorted(self.default_parameter) + ] + return f"{self.name}({', '.join(para_strs)})" diff --git a/GSTools/source/src/gstools/normalizer/methods.py b/GSTools/source/src/gstools/normalizer/methods.py new file mode 100644 index 0000000000000000000000000000000000000000..ded75eb7d931175e9a3f1d8aa9fc1afd7f11dd44 --- /dev/null +++ b/GSTools/source/src/gstools/normalizer/methods.py @@ -0,0 +1,362 @@ +""" +GStools subpackage providing different normalizer transformations. + +.. currentmodule:: gstools.normalizer.methods + +The following classes are provided + +.. autosummary:: + LogNormal + BoxCox + BoxCoxShift + YeoJohnson + Modulus + Manly +""" + +import numpy as np + +from gstools.normalizer.base import Normalizer + + +class LogNormal(Normalizer): + r"""Log-normal fields. + + Notes + ----- + This parameter-free transformation is given by: + + .. math:: + y=\log(x) + """ + + normalize_range = (0.0, np.inf) + """Valid range for input data.""" + + def _denormalize(self, data): + return np.exp(data) + + def _normalize(self, data): + return np.log(data) + + def _derivative(self, data): + return np.power(data, -1) + + +class BoxCox(Normalizer): + r"""Box-Cox (1964) transformed fields. + + Parameters + ---------- + data : array_like, optional + Input data to fit the transformation in order to gain normality. + The default is None. + lmbda : :class:`float`, optional + Shape parameter. Default: 1 + + Notes + ----- + This transformation is given by [Box1964]_: + + .. math:: + y=\begin{cases} + \frac{x^{\lambda} - 1}{\lambda} & \lambda\neq 0 \\ + \log(x) & \lambda = 0 + \end{cases} + + References + ---------- + .. [Box1964] G.E.P. Box and D.R. Cox, + "An Analysis of Transformations", + Journal of the Royal Statistical Society B, 26, 211-252, (1964) + """ + + default_parameter = {"lmbda": 1} + """:class:`dict`: Default parameter of the BoxCox-Normalizer.""" + normalize_range = (0.0, np.inf) + """:class:`tuple`: Valid range for input data.""" + + @property + def denormalize_range(self): + """:class:`tuple`: Valid range for output data depending on lmbda. + + `(-1/lmbda, inf)` or `(-inf, -1/lmbda)` + """ + if np.isclose(self.lmbda, 0): + return (-np.inf, np.inf) + if self.lmbda < 0: + return (-np.inf, -np.divide(1, self.lmbda)) + return (-np.divide(1, self.lmbda), np.inf) + + def _denormalize(self, data): + if np.isclose(self.lmbda, 0): + return np.exp(data) + return (1 + np.multiply(data, self.lmbda)) ** (1 / self.lmbda) + + def _normalize(self, data): + if np.isclose(self.lmbda, 0): + return np.log(data) + return (np.power(data, self.lmbda) - 1) / self.lmbda + + def _derivative(self, data): + return np.power(data, self.lmbda - 1) + + +class BoxCoxShift(Normalizer): + r"""Box-Cox (1964) transformed fields including shifting. + + Parameters + ---------- + data : array_like, optional + Input data to fit the transformation in order to gain normality. + The default is None. + lmbda : :class:`float`, optional + Shape parameter. Default: 1 + shift : :class:`float`, optional + Shift parameter. Default: 0 + + Notes + ----- + This transformation is given by [Box1964]_: + + .. math:: + y=\begin{cases} + \frac{(x+s)^{\lambda} - 1}{\lambda} & \lambda\neq 0 \\ + \log(x+s) & \lambda = 0 + \end{cases} + + Fitting the shift parameter is rather hard. You should consider skipping + "shift" during fitting: + + >>> data = range(5) + >>> norm = BoxCoxShift(shift=0.5) + >>> norm.fit(data, skip=["shift"]) + {'shift': 0.5, 'lmbda': 0.6747515267420799} + + References + ---------- + .. [Box1964] G.E.P. Box and D.R. Cox, + "An Analysis of Transformations", + Journal of the Royal Statistical Society B, 26, 211-252, (1964) + """ + + default_parameter = {"shift": 0, "lmbda": 1} + """:class:`dict`: Default parameters of the BoxCoxShift-Normalizer.""" + + @property + def normalize_range(self): + """:class:`tuple`: Valid range for input data depending on shift. + + `(-shift, inf)` + """ + return (-self.shift, np.inf) + + @property + def denormalize_range(self): + """:class:`tuple`: Valid range for output data depending on lmbda. + + `(-1/lmbda, inf)` or `(-inf, -1/lmbda)` + """ + if np.isclose(self.lmbda, 0): + return (-np.inf, np.inf) + if self.lmbda < 0: + return (-np.inf, -np.divide(1, self.lmbda)) + return (-np.divide(1, self.lmbda), np.inf) + + def _denormalize(self, data): + if np.isclose(self.lmbda, 0): + return np.exp(data) - self.shift + return (1 + np.multiply(data, self.lmbda)) ** ( + 1 / self.lmbda + ) - self.shift + + def _normalize(self, data): + if np.isclose(self.lmbda, 0): + return np.log(np.add(data, self.shift)) + return (np.add(data, self.shift) ** self.lmbda - 1) / self.lmbda + + def _derivative(self, data): + return np.power(np.add(data, self.shift), self.lmbda - 1) + + +class YeoJohnson(Normalizer): + r"""Yeo-Johnson (2000) transformed fields. + + Parameters + ---------- + data : array_like, optional + Input data to fit the transformation in order to gain normality. + The default is None. + lmbda : :class:`float`, optional + Shape parameter. Default: 1 + + Notes + ----- + This transformation is given by [Yeo2000]_: + + .. math:: + y=\begin{cases} + \frac{(x+1)^{\lambda} - 1}{\lambda} + & x\geq 0,\, \lambda\neq 0 \\ + \log(x+1) + & x\geq 0,\, \lambda = 0 \\ + -\frac{(|x|+1)^{2-\lambda} - 1}{2-\lambda} + & x<0,\, \lambda\neq 2 \\ + -\log(|x|+1) + & x<0,\, \lambda = 2 + \end{cases} + + + References + ---------- + .. [Yeo2000] I.K. Yeo and R.A. Johnson, + "A new family of power transformations to improve normality or + symmetry." Biometrika, 87(4), pp.954-959, (2000). + """ + + default_parameter = {"lmbda": 1} + """:class:`dict`: Default parameter of the YeoJohnson-Normalizer.""" + + def _denormalize(self, data): + data = np.asanyarray(data) + res = np.zeros_like(data, dtype=np.double) + pos = data >= 0 + # when data >= 0 + if np.isclose(self.lmbda, 0): + res[pos] = np.expm1(data[pos]) + else: # self.lmbda != 0 + res[pos] = np.power(data[pos] * self.lmbda + 1, 1 / self.lmbda) - 1 + # when data < 0 + if np.isclose(self.lmbda, 2): + res[~pos] = -np.expm1(-data[~pos]) + else: # self.lmbda != 2 + res[~pos] = 1 - np.power( + -(2 - self.lmbda) * data[~pos] + 1, 1 / (2 - self.lmbda) + ) + return res + + def _normalize(self, data): + data = np.asanyarray(data) + res = np.zeros_like(data, dtype=np.double) + pos = data >= 0 + # when data >= 0 + if np.isclose(self.lmbda, 0): + res[pos] = np.log1p(data[pos]) + else: # self.lmbda != 0 + res[pos] = (np.power(data[pos] + 1, self.lmbda) - 1) / self.lmbda + # when data < 0 + if np.isclose(self.lmbda, 2): + res[~pos] = -np.log1p(-data[~pos]) + else: # self.lmbda != 2 + res[~pos] = -(np.power(-data[~pos] + 1, 2 - self.lmbda) - 1) / ( + 2 - self.lmbda + ) + return res + + def _derivative(self, data): + return (np.abs(data) + 1) ** (np.sign(data) * (self.lmbda - 1)) + + +class Modulus(Normalizer): + r"""Modulus or John-Draper (1980) transformed fields. + + Parameters + ---------- + data : array_like, optional + Input data to fit the transformation in order to gain normality. + The default is None. + lmbda : :class:`float`, optional + Shape parameter. Default: 1 + + Notes + ----- + This transformation is given by [John1980]_: + + .. math:: + y=\begin{cases} + \mathrm{sgn}(x)\frac{(|x|+1)^{\lambda} - 1}{\lambda} & \lambda\neq 0 \\ + \mathrm{sgn}(x)\log(|x|+1) & \lambda = 0 + \end{cases} + + References + ---------- + .. [John1980] J. A. John, and N. R. Draper, + "An Alternative Family of Transformations." Journal + of the Royal Statistical Society C, 29.2, 190-197, (1980) + """ + + default_parameter = {"lmbda": 1} + """:class:`dict`: Default parameter of the Modulus-Normalizer.""" + + def _denormalize(self, data): + if np.isclose(self.lmbda, 0): + return np.sign(data) * np.expm1(np.abs(data)) + return np.sign(data) * ( + (1 + self.lmbda * np.abs(data)) ** (1 / self.lmbda) - 1 + ) + + def _normalize(self, data): + if np.isclose(self.lmbda, 0): + return np.sign(data) * np.log1p(np.abs(data)) + return ( + np.sign(data) * ((np.abs(data) + 1) ** self.lmbda - 1) / self.lmbda + ) + + def _derivative(self, data): + return np.power(np.abs(data) + 1, self.lmbda - 1) + + +class Manly(Normalizer): + r"""Manly (1971) transformed fields. + + Parameters + ---------- + data : array_like, optional + Input data to fit the transformation in order to gain normality. + The default is None. + lmbda : :class:`float`, optional + Shape parameter. Default: 1 + + Notes + ----- + This transformation is given by [Manly1976]_: + + .. math:: + y=\begin{cases} + \frac{\exp(\lambda x) - 1}{\lambda} & \lambda\neq 0 \\ + x & \lambda = 0 + \end{cases} + + References + ---------- + .. [Manly1976] B. F. J. Manly, "Exponential data transformations.", + Journal of the Royal Statistical Society D, 25.1, 37-42 (1976). + """ + + default_parameter = {"lmbda": 1} + """:class:`dict`: Default parameter of the Manly-Normalizer.""" + + @property + def denormalize_range(self): + """:class:`tuple`: Valid range for output data depending on lmbda. + + `(-1/lmbda, inf)` or `(-inf, -1/lmbda)` + """ + if np.isclose(self.lmbda, 0): + return (-np.inf, np.inf) + if self.lmbda < 0: + return (-np.inf, np.divide(1, self.lmbda)) + return (-np.divide(1, self.lmbda), np.inf) + + def _denormalize(self, data): + if np.isclose(self.lmbda, 0): + return data + return np.log1p(np.multiply(data, self.lmbda)) / self.lmbda + + def _normalize(self, data): + if np.isclose(self.lmbda, 0): + return data + return np.expm1(np.multiply(data, self.lmbda)) / self.lmbda + + def _derivative(self, data): + return np.exp(np.multiply(data, self.lmbda)) diff --git a/GSTools/source/src/gstools/normalizer/tools.py b/GSTools/source/src/gstools/normalizer/tools.py new file mode 100644 index 0000000000000000000000000000000000000000..3e395d290c6a48402962e888222854c5e971bc5a --- /dev/null +++ b/GSTools/source/src/gstools/normalizer/tools.py @@ -0,0 +1,186 @@ +""" +GStools subpackage providing tools for Normalizers. + +.. currentmodule:: gstools.normalizer.tools + +The following classes and functions are provided + +.. autosummary:: + apply_mean_norm_trend + remove_trend_norm_mean +""" + +import numpy as np + +from gstools.normalizer.base import Normalizer +from gstools.tools.geometric import ( + format_struct_pos_shape, + format_unstruct_pos_shape, +) +from gstools.tools.misc import eval_func + +__all__ = ["apply_mean_norm_trend", "remove_trend_norm_mean"] + + +def _check_normalizer(normalizer): + if isinstance(normalizer, type) and issubclass(normalizer, Normalizer): + normalizer = normalizer() + elif normalizer is None: + normalizer = Normalizer() + elif not isinstance(normalizer, Normalizer): + raise ValueError("Check: 'normalizer' not of type 'Normalizer'.") + return normalizer + + +def apply_mean_norm_trend( + pos, + field, + mean=None, + normalizer=None, + trend=None, + mesh_type="unstructured", + value_type="scalar", + check_shape=True, + stacked=False, +): + """ + Apply mean, de-normalization and trend to given field. + + Parameters + ---------- + pos : :any:`iterable` + Position tuple, containing main direction and transversal directions. + field : :class:`numpy.ndarray` or :class:`list` of :class:`numpy.ndarray` + The spatially distributed data. + You can pass a list of fields, that will be used simultaneously. + Then you need to set ``stacked=True``. + mean : :any:`None` or :class:`float` or :any:`callable`, optional + Mean of the field if wanted. Could also be a callable. + The default is None. + normalizer : :any:`None` or :any:`Normalizer`, optional + Normalizer to be applied to the field. + The default is None. + trend : :any:`None` or :class:`float` or :any:`callable`, optional + Trend of the denormalized fields. If no normalizer is applied, + this behaves equal to 'mean'. + The default is None. + mesh_type : :class:`str`, optional + 'structured' / 'unstructured' + Default: 'unstructured' + value_type : :class:`str`, optional + Value type of the field. Either "scalar" or "vector". + The default is "scalar". + check_shape : :class:`bool`, optional + Whether to check pos and field shapes. The default is True. + stacked : :class:`bool`, optional + Whether the field is stacked or not. The default is False. + + Returns + ------- + field : :class:`numpy.ndarray` + The transformed field. + """ + normalizer = _check_normalizer(normalizer) + if check_shape: + if mesh_type != "unstructured": + pos, shape, dim = format_struct_pos_shape( + pos, field.shape, check_stacked_shape=stacked + ) + else: + pos, shape, dim = format_unstruct_pos_shape( + pos, field.shape, check_stacked_shape=stacked + ) + field = np.asarray(field, dtype=np.double).reshape(shape) + else: + dim = len(pos) + if not stacked: + field = [field] + field_cnt = len(field) + for i in range(field_cnt): + field[i] += eval_func(mean, pos, dim, mesh_type, value_type, True) + field = normalizer.denormalize(field) + for i in range(field_cnt): + field[i] += eval_func(trend, pos, dim, mesh_type, value_type, True) + return field if stacked else field[0] + + +def remove_trend_norm_mean( + pos, + field, + mean=None, + normalizer=None, + trend=None, + mesh_type="unstructured", + value_type="scalar", + check_shape=True, + stacked=False, + fit_normalizer=False, +): + """ + Remove trend, de-normalization and mean from given field. + + Parameters + ---------- + pos : :any:`iterable` + Position tuple, containing main direction and transversal directions. + field : :class:`numpy.ndarray` or :class:`list` of :class:`numpy.ndarray` + The spatially distributed data. + You can pass a list of fields, that will be used simultaneously. + Then you need to set ``stacked=True``. + mean : :any:`None` or :class:`float` or :any:`callable`, optional + Mean of the field if wanted. Could also be a callable. + The default is None. + normalizer : :any:`None` or :any:`Normalizer`, optional + Normalizer to be applied to the field. + The default is None. + trend : :any:`None` or :class:`float` or :any:`callable`, optional + Trend of the denormalized fields. If no normalizer is applied, + this behaves equal to 'mean'. + The default is None. + mesh_type : :class:`str`, optional + 'structured' / 'unstructured' + Default: 'unstructured' + value_type : :class:`str`, optional + Value type of the field. Either "scalar" or "vector". + The default is "scalar". + check_shape : :class:`bool`, optional + Whether to check pos and field shapes. The default is True. + stacked : :class:`bool`, optional + Whether the field is stacked or not. The default is False. + fit_normalizer : :class:`bool`, optional + Whether to fit the data-normalizer to the given (detrended) field. + Default: False + + Returns + ------- + field : :class:`numpy.ndarray` + The cleaned field. + normalizer : :any:`Normalizer`, optional + The fitted normalizer for the given data. + Only provided if `fit_normalizer` is True. + """ + normalizer = _check_normalizer(normalizer) + if check_shape: + if mesh_type != "unstructured": + pos, shape, dim = format_struct_pos_shape( + pos, field.shape, check_stacked_shape=stacked + ) + else: + pos, shape, dim = format_unstruct_pos_shape( + pos, field.shape, check_stacked_shape=stacked + ) + field = np.asarray(field, dtype=np.double).reshape(shape) + else: + dim = len(pos) + if not stacked: + field = [field] + field_cnt = len(field) + for i in range(field_cnt): + field[i] -= eval_func(trend, pos, dim, mesh_type, value_type, True) + if fit_normalizer: + normalizer.fit(field) + field = normalizer.normalize(field) + for i in range(field_cnt): + field[i] -= eval_func(mean, pos, dim, mesh_type, value_type, True) + out = field if stacked else field[0] + return (out, normalizer) if fit_normalizer else out diff --git a/GSTools/source/src/gstools/random/__init__.py b/GSTools/source/src/gstools/random/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..af8f73786bc02a81e4ba8d0a776f222328712be2 --- /dev/null +++ b/GSTools/source/src/gstools/random/__init__.py @@ -0,0 +1,36 @@ +""" +GStools subpackage for random number generation. + +.. currentmodule:: gstools.random + +Random Number Generator +^^^^^^^^^^^^^^^^^^^^^^^ + +.. autosummary:: + :toctree: + + RNG + +Seed Generator +^^^^^^^^^^^^^^ + +.. autosummary:: + :toctree: + + MasterRNG + +Distribution factory +^^^^^^^^^^^^^^^^^^^^ + +.. autosummary:: + :toctree: + + dist_gen + +---- +""" + +from gstools.random.rng import RNG +from gstools.random.tools import MasterRNG, dist_gen + +__all__ = ["RNG", "MasterRNG", "dist_gen"] diff --git a/GSTools/source/src/gstools/random/rng.py b/GSTools/source/src/gstools/random/rng.py new file mode 100644 index 0000000000000000000000000000000000000000..db4baef939c4506620e2a89b67a24402f16de37b --- /dev/null +++ b/GSTools/source/src/gstools/random/rng.py @@ -0,0 +1,220 @@ +""" +GStools subpackage providing the core of the spatial random field generation. + +.. currentmodule:: gstools.random.rng + +The following classes are provided + +.. autosummary:: + RNG +""" + +import emcee as mc +import numpy as np +import numpy.random as rand +from emcee.state import State + +from gstools.random.tools import MasterRNG, dist_gen + +__all__ = ["RNG"] + + +class RNG: + """ + A random number generator for different distributions and multiple streams. + + Parameters + ---------- + seed : :class:`int` or :any:`None`, optional + The seed of the master RNG, if ``None``, + a random seed is used. Default: ``None`` + """ + + def __init__(self, seed=None): + # set seed + self._master_rng = None + self.seed = seed + + def sample_ln_pdf( + self, + ln_pdf, + size=None, + sample_around=1.0, + nwalkers=50, + burn_in=20, + oversampling_factor=10, + ): + """Sample from a distribution given by ln(pdf). + + This algorithm uses the :class:`emcee.EnsembleSampler` + + Parameters + ---------- + ln_pdf : :any:`callable` + The logarithm of the Probability density function + of the given distribution, that takes a single argument + size : :class:`int` or :any:`None`, optional + sample size. Default: None + sample_around : :class:`float`, optional + Starting point for initial guess Default: 1. + nwalkers : :class:`int`, optional + The number of walkers in the mcmc sampler. Used for the + emcee.EnsembleSampler class. + Default: 50 + burn_in : :class:`int`, optional + Number of burn-in runs in the mcmc algorithm. + Default: 20 + oversampling_factor : :class:`int`, optional + To guess the sample number needed for proper results, we use a + factor for oversampling. The intern used sample-size is + calculated by + + ``sample_size = max(burn_in, (size/nwalkers)*oversampling_factor)`` + + So at least, as much as the burn-in runs. + Default: 10 + """ + if size is None: # pragma: no cover + sample_size = burn_in + else: + sample_size = max(burn_in, (size / nwalkers) * oversampling_factor) + # sample_size needs to be integer for emcee >= 3.1 + sample_size = int(sample_size) + # initial guess + init_guess = ( + self.random.rand(nwalkers).reshape((nwalkers, 1)) * sample_around + ) + # initialize the sampler + sampler = mc.EnsembleSampler(nwalkers, 1, ln_pdf, vectorize=True) + # burn in phase with saving of last position + initial_state = State(init_guess, copy=True) + initial_state.random_state = self.random.get_state() + burn_in_state = sampler.run_mcmc( + initial_state=initial_state, nsteps=burn_in + ) + # reset after burn_in + sampler.reset() + # actual sampling + initial_state = State(burn_in_state, copy=True) + initial_state.random_state = self.random.get_state() + sampler.run_mcmc(initial_state=initial_state, nsteps=sample_size) + samples = sampler.get_chain(flat=True)[:, 0] + + # choose samples according to size + return self.random.choice(samples, size) + + def sample_dist(self, pdf=None, cdf=None, ppf=None, size=None, **kwargs): + """Sample from a distribution given by pdf, cdf and/or ppf. + + Parameters + ---------- + pdf : :any:`callable` or :any:`None`, optional + Probability density function of the given distribution, + that takes a single argument + Default: ``None`` + cdf : :any:`callable` or :any:`None`, optional + Cumulative distribution function of the given distribution, that + takes a single argument + Default: ``None`` + ppf : :any:`callable` or :any:`None`, optional + Percent point function of the given distribution, that + takes a single argument + Default: ``None`` + size : :class:`int` or :any:`None`, optional + sample size. Default: None + **kwargs + Keyword-arguments that are forwarded to + :any:`scipy.stats.rv_continuous`. + + Returns + ------- + samples : :class:`float` or :class:`numpy.ndarray` + the samples from the given distribution + + Notes + ----- + At least pdf or cdf needs to be given. + """ + kwargs["seed"] = self.random + dist = dist_gen(pdf_in=pdf, cdf_in=cdf, ppf_in=ppf, **kwargs) + return dist.rvs(size=size) + + def sample_sphere(self, dim, size=None): + """Uniform sampling on a d-dimensional sphere. + + Parameters + ---------- + dim : :class:`int` + Dimension of the sphere. Just 1, 2, and 3 supported. + size : :class:`int`, optional + sample size + + Returns + ------- + coord : :class:`numpy.ndarray` + x[, y[, z]] coordinates on the sphere with shape (dim, size) + """ + if size is None: # pragma: no cover + coord = np.empty((dim, 1), dtype=np.double) + else: + coord = np.empty( # saver conversion of size to resulting shape + (dim,) + tuple(np.atleast_1d(size)), dtype=np.double + ) + if dim == 1: + coord[0] = self.random.choice([-1, 1], size=size) + elif dim == 2: + ang1 = self.random.uniform(0.0, 2 * np.pi, size) + coord[0] = np.cos(ang1) + coord[1] = np.sin(ang1) + elif dim == 3: + ang1 = self.random.uniform(0.0, 2 * np.pi, size) + ang2 = self.random.uniform(-1.0, 1.0, size) + coord[0] = np.sqrt(1.0 - ang2**2) * np.cos(ang1) + coord[1] = np.sqrt(1.0 - ang2**2) * np.sin(ang1) + coord[2] = ang2 + else: # pragma: no cover + # http://corysimon.github.io/articles/uniformdistn-on-sphere/ + coord = self.random.normal(size=coord.shape) + while True: # loop until all norms are non-zero + norm = np.linalg.norm(coord, axis=0) + # check for zero norms + zero_norms = np.isclose(norm, 0) + # exit the loop if all norms are non-zero + if not np.any(zero_norms): + break + # transpose, since the next transpose reverses axis order + zero_samples = zero_norms.T.nonzero() + # need to transpose to have dim-axis last + new_shape = coord.T[zero_samples].shape + # resample the zero norm samples + coord.T[zero_samples] = self.random.normal(size=new_shape) + # project onto sphere + coord = coord / norm + return np.reshape(coord, dim) if size is None else coord + + @property + def random(self): + """:any:`numpy.random.RandomState`: Randomstate. + + Get a stream to the numpy Random number generator. + You can use this, to call any provided distribution + from :any:`numpy.random.RandomState`. + """ + return rand.RandomState(self._master_rng()) + + @property # pragma: no cover + def seed(self): + """:class:`int`: Seed of the master RNG. + + The setter property not only saves the new seed, but also creates + a new master RNG function with the new seed. + """ + return self._master_rng.seed + + @seed.setter + def seed(self, new_seed=None): + self._master_rng = MasterRNG(new_seed) + + def __repr__(self): + """Return String representation.""" + return f"RNG(seed={self.seed})" diff --git a/GSTools/source/src/gstools/random/tools.py b/GSTools/source/src/gstools/random/tools.py new file mode 100644 index 0000000000000000000000000000000000000000..d61327ea773be178e2bc24b9355b42a611ebb4f5 --- /dev/null +++ b/GSTools/source/src/gstools/random/tools.py @@ -0,0 +1,183 @@ +""" +GStools subpackage providing tools for random sampling. + +.. currentmodule:: gstools.random.tools + +The following classes are provided + +.. autosummary:: + MasterRNG + dist_gen +""" + +import numpy.random as rand +from scipy.stats import rv_continuous + +__all__ = ["MasterRNG", "dist_gen"] + + +class MasterRNG: + """Master random number generator for generating seeds. + + Parameters + ---------- + seed : :class:`int` or :any:`None`, optional + The seed of the master RNG, if ``None``, + a random seed is used. Default: ``None`` + + """ + + def __init__(self, seed): + self._seed = seed + self._master_rng_fct = rand.RandomState(seed) + self._master_rng = lambda: self._master_rng_fct.randint(1, 2**16) + + def __call__(self): + """Return a random seed.""" + return self._master_rng() + + @property # pragma: no cover + def seed(self): + """:class:`int`: Seed of the master RNG. + + The setter property not only saves the new seed, but also creates + a new master RNG function with the new seed. + """ + return self._seed + + def __repr__(self): + """Return String representation.""" + return f"MasterRNG(seed={self.seed})" + + +def dist_gen(pdf_in=None, cdf_in=None, ppf_in=None, **kwargs): + """Distribution Factory. + + Parameters + ---------- + pdf_in : :any:`callable` or :any:`None`, optional + Probability distribution function of the given distribution, that + takes a single argument + Default: ``None`` + cdf_in : :any:`callable` or :any:`None`, optional + Cumulative distribution function of the given distribution, that + takes a single argument + Default: ``None`` + ppf_in : :any:`callable` or :any:`None`, optional + Percent point function of the given distribution, that + takes a single argument + Default: ``None`` + **kwargs + Keyword-arguments forwarded to :any:`scipy.stats.rv_continuous`. + + Returns + ------- + dist : :class:`scipy.stats.rv_continuous` + The constructed distribution. + + Notes + ----- + At least pdf or cdf needs to be given. + """ + if ppf_in is None: + if pdf_in is not None and cdf_in is None: + return DistPdf(pdf_in, **kwargs) + if pdf_in is None and cdf_in is not None: + return DistCdf(cdf_in, **kwargs) + if pdf_in is not None and cdf_in is not None: + return DistPdfCdf(pdf_in, cdf_in, **kwargs) + raise ValueError("Either pdf or cdf must be given") + + if pdf_in is not None and cdf_in is None: + return DistPdfPpf(pdf_in, ppf_in, **kwargs) + if pdf_in is None and cdf_in is not None: + return DistCdfPpf(cdf_in, ppf_in, **kwargs) + if pdf_in is not None and cdf_in is not None: + return DistPdfCdfPpf(pdf_in, cdf_in, ppf_in, **kwargs) + raise ValueError("pdf or cdf must be given along with the ppf") + + +class DistPdf(rv_continuous): + """Generate distribution from pdf.""" + + def __init__(self, pdf_in, **kwargs): + self.pdf_in = pdf_in + super().__init__(**kwargs) + + def _pdf(self, x, *args): + return self.pdf_in(x) + + +class DistCdf(rv_continuous): + """Generate distribution from cdf.""" + + def __init__(self, cdf_in, **kwargs): + self.cdf_in = cdf_in + super().__init__(**kwargs) + + def _cdf(self, x, *args): + return self.cdf_in(x) + + +class DistPdfCdf(rv_continuous): + """Generate distribution from pdf and cdf.""" + + def __init__(self, pdf_in, cdf_in, **kwargs): + self.pdf_in = pdf_in + self.cdf_in = cdf_in + super().__init__(**kwargs) + + def _pdf(self, x, *args): + return self.pdf_in(x) + + def _cdf(self, x, *args): + return self.cdf_in(x) + + +class DistPdfPpf(rv_continuous): + """Generate distribution from pdf and ppf.""" + + def __init__(self, pdf_in, ppf_in, **kwargs): + self.pdf_in = pdf_in + self.ppf_in = ppf_in + super().__init__(**kwargs) + + def _pdf(self, x, *args): + return self.pdf_in(x) + + def _ppf(self, q, *args): + return self.ppf_in(q) + + +class DistCdfPpf(rv_continuous): + """Generate distribution from cdf and ppf.""" + + def __init__(self, cdf_in, ppf_in, **kwargs): + self.cdf_in = cdf_in + self.ppf_in = ppf_in + super().__init__(**kwargs) + + def _cdf(self, x, *args): + return self.cdf_in(x) + + def _ppf(self, q, *args): + return self.ppf_in(q) + + +class DistPdfCdfPpf(rv_continuous): + """Generate distribution from pdf, cdf and ppf.""" + + def __init__(self, pdf_in, cdf_in, ppf_in, **kwargs): + self.pdf_in = pdf_in + self.cdf_in = cdf_in + self.ppf_in = ppf_in + super().__init__(**kwargs) + + def _pdf(self, x, *args): + return self.pdf_in(x) + + def _cdf(self, x, *args): + return self.cdf_in(x) + + def _ppf(self, q, *args): + return self.ppf_in(q) diff --git a/GSTools/source/src/gstools/tools/__init__.py b/GSTools/source/src/gstools/tools/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1f68dbaf16558a40edb15dbf680c067cb99477b5 --- /dev/null +++ b/GSTools/source/src/gstools/tools/__init__.py @@ -0,0 +1,159 @@ +""" +GStools subpackage providing miscellaneous tools. + +.. currentmodule:: gstools.tools + +Export +^^^^^^ + +.. autosummary:: + :toctree: + + vtk_export + vtk_export_structured + vtk_export_unstructured + to_vtk + to_vtk_structured + to_vtk_unstructured + +Special functions +^^^^^^^^^^^^^^^^^ + +.. autosummary:: + :toctree: + + confidence_scaling + inc_gamma + inc_gamma_low + exp_int + inc_beta + tplstable_cor + tpl_exp_spec_dens + tpl_gau_spec_dens + +Geometric +^^^^^^^^^ + +.. autosummary:: + :toctree: + + rotated_main_axes + set_angles + set_anis + no_of_angles + rotation_planes + givens_rotation + matrix_rotate + matrix_derotate + matrix_isotropify + matrix_anisotropify + matrix_isometrize + matrix_anisometrize + ang2dir + generate_grid + generate_st_grid + +Misc +^^^^ + +.. autosummary:: + EARTH_RADIUS + KM_SCALE + DEGREE_SCALE + RADIAN_SCALE + +---- + +.. autodata:: EARTH_RADIUS + +.. autodata:: KM_SCALE + +.. autodata:: DEGREE_SCALE + +.. autodata:: RADIAN_SCALE +""" + +from gstools.tools.export import ( + to_vtk, + to_vtk_structured, + to_vtk_unstructured, + vtk_export, + vtk_export_structured, + vtk_export_unstructured, +) +from gstools.tools.geometric import ( + ang2dir, + generate_grid, + generate_st_grid, + givens_rotation, + matrix_anisometrize, + matrix_anisotropify, + matrix_derotate, + matrix_isometrize, + matrix_isotropify, + matrix_rotate, + no_of_angles, + rotated_main_axes, + rotation_planes, + set_angles, + set_anis, +) +from gstools.tools.special import ( + confidence_scaling, + exp_int, + inc_beta, + inc_gamma, + inc_gamma_low, + tpl_exp_spec_dens, + tpl_gau_spec_dens, + tplstable_cor, +) + +EARTH_RADIUS = 6371.0 +"""float: earth radius for WGS84 ellipsoid in km""" + +KM_SCALE = 6371.0 +"""float: earth radius for WGS84 ellipsoid in km""" + +DEGREE_SCALE = 57.29577951308232 +"""float: radius for unit sphere in degree""" + +RADIAN_SCALE = 1.0 +"""float: radius for unit sphere""" + + +__all__ = [ + "vtk_export", + "vtk_export_structured", + "vtk_export_unstructured", + "to_vtk", + "to_vtk_structured", + "to_vtk_unstructured", + "confidence_scaling", + "inc_gamma", + "inc_gamma_low", + "exp_int", + "inc_beta", + "tplstable_cor", + "tpl_exp_spec_dens", + "tpl_gau_spec_dens", + "set_angles", + "set_anis", + "no_of_angles", + "rotation_planes", + "givens_rotation", + "matrix_rotate", + "matrix_derotate", + "matrix_isotropify", + "matrix_anisotropify", + "matrix_isometrize", + "matrix_anisometrize", + "rotated_main_axes", + "ang2dir", + "generate_grid", + "generate_st_grid", + "EARTH_RADIUS", + "KM_SCALE", + "DEGREE_SCALE", + "RADIAN_SCALE", +] diff --git a/GSTools/source/src/gstools/tools/export.py b/GSTools/source/src/gstools/tools/export.py new file mode 100644 index 0000000000000000000000000000000000000000..9965a24464a06e8bc09bcce4c7a0d36912305d2e --- /dev/null +++ b/GSTools/source/src/gstools/tools/export.py @@ -0,0 +1,235 @@ +""" +GStools subpackage providing export routines. + +.. currentmodule:: gstools.tools.export + +The following functions are provided + +.. autosummary:: + vtk_export + vtk_export_structured + vtk_export_unstructured + to_vtk + to_vtk_structured + to_vtk_unstructured +""" + +import numpy as np +from pyevtk.hl import gridToVTK, pointsToVTK + +try: + import pyvista as pv +except ImportError: + pv = None + +__all__ = [ + "to_vtk_structured", + "vtk_export_structured", + "to_vtk_unstructured", + "vtk_export_unstructured", + "to_vtk", + "vtk_export", +] + + +# export routines ############################################################# + + +def _vtk_structured_helper(pos, fields): + """Extract field info for vtk rectilinear grid.""" + if not isinstance(fields, dict): + fields = {"field": fields} + if len(pos) > 3: + raise ValueError( + "gstools.vtk_export_structured: " + "vtk export only possible for dim=1,2,3" + ) + x = pos[0] + y = pos[1] if len(pos) > 1 else np.array([0]) + z = pos[2] if len(pos) > 2 else np.array([0]) + # need fortran order in VTK + for field in fields: + fields[field] = fields[field].reshape(-1, order="F") + if len(fields[field]) != len(x) * len(y) * len(z): + raise ValueError( + "gstools.vtk_export_structured: " + "field shape doesn't match the given mesh" + ) + return x, y, z, fields + + +def to_vtk_structured(pos, fields): # pragma: no cover + """Create a vtk structured rectilinear grid from a field. + + Parameters + ---------- + pos : :class:`list` + the position tuple, containing main direction and transversal + directions + fields : :class:`dict` or :class:`numpy.ndarray` + Structured fields to be saved. + Either a single numpy array as returned by SRF, + or a dictionary of fields with theirs names as keys. + + Returns + ------- + :class:`pyvista.RectilinearGrid` + A PyVista rectilinear grid of the structured field data. Data arrays + live on the point data of this PyVista dataset. + """ + x, y, z, fields = _vtk_structured_helper(pos=pos, fields=fields) + if pv is not None: + grid = pv.RectilinearGrid(x, y, z) + grid.point_data.update(fields) + else: + raise ImportError("Please install PyVista to create VTK datasets.") + return grid + + +def vtk_export_structured(filename, pos, fields): # pragma: no cover + """Export a field to vtk structured rectilinear grid file. + + Parameters + ---------- + filename : :class:`str` + Filename of the file to be saved, including the path. Note that an + ending (.vtr) will be added to the name. + pos : :class:`list` + the position tuple, containing main direction and transversal + directions + fields : :class:`dict` or :class:`numpy.ndarray` + Structured fields to be saved. + Either a single numpy array as returned by SRF, + or a dictionary of fields with theirs names as keys. + """ + x, y, z, fields = _vtk_structured_helper(pos=pos, fields=fields) + return gridToVTK(filename, x, y, z, pointData=fields) + + +def _vtk_unstructured_helper(pos, fields): + if not isinstance(fields, dict): + fields = {"field": fields} + if len(pos) > 3: + raise ValueError( + "gstools.vtk_export_structured: " + "vtk export only possible for dim=1,2,3" + ) + x = pos[0] + y = pos[1] if len(pos) > 1 else np.zeros_like(x) + z = pos[2] if len(pos) > 2 else np.zeros_like(x) + for field in fields: + fields[field] = fields[field].reshape(-1) + if ( + len(fields[field]) != len(x) + or len(fields[field]) != len(y) + or len(fields[field]) != len(z) + ): + raise ValueError( + "gstools.vtk_export_unstructured: " + "field shape doesn't match the given mesh" + ) + return x, y, z, fields + + +def to_vtk_unstructured(pos, fields): # pragma: no cover + """Export a field to vtk structured rectilinear grid file. + + Parameters + ---------- + pos : :class:`list` + the position tuple, containing main direction and transversal + directions + fields : :class:`dict` or :class:`numpy.ndarray` + Unstructured fields to be saved. + Either a single numpy array as returned by SRF, + or a dictionary of fields with theirs names as keys. + + Returns + ------- + :class:`pyvista.UnstructuredGrid` + A PyVista unstructured grid of the unstructured field data. Data arrays + live on the point data of this PyVista dataset. This is essentially + a point cloud with no topology. + """ + x, y, z, fields = _vtk_unstructured_helper(pos=pos, fields=fields) + if pv is not None: + grid = pv.PolyData(np.c_[x, y, z]).cast_to_unstructured_grid() + grid.point_data.update(fields) + else: + raise ImportError("Please install PyVista to create VTK datasets.") + return grid + + +def vtk_export_unstructured(filename, pos, fields): # pragma: no cover + """Export a field to vtk unstructured grid file. + + Parameters + ---------- + filename : :class:`str` + Filename of the file to be saved, including the path. Note that an + ending (.vtu) will be added to the name. + pos : :class:`list` + the position tuple, containing main direction and transversal + directions + fields : :class:`dict` or :class:`numpy.ndarray` + Unstructured fields to be saved. + Either a single numpy array as returned by SRF, + or a dictionary of fields with theirs names as keys. + """ + x, y, z, fields = _vtk_unstructured_helper(pos=pos, fields=fields) + return pointsToVTK(filename, x, y, z, data=fields) + + +def to_vtk(pos, fields, mesh_type="unstructured"): # pragma: no cover + """Create a VTK/PyVista grid. + + Parameters + ---------- + pos : :class:`list` + the position tuple, containing main direction and transversal + directions + fields : :class:`dict` or :class:`numpy.ndarray` + [Un]structured fields to be saved. + Either a single numpy array as returned by SRF, + or a dictionary of fields with theirs names as keys. + mesh_type : :class:`str`, optional + 'structured' / 'unstructured'. Default: structured + + Returns + ------- + :class:`pyvista.RectilinearGrid` or :class:`pyvista.UnstructuredGrid` + This will return a PyVista object for the given field data in its + appropriate type. Structured meshes will return a + :class:`pyvista.RectilinearGrid` and unstructured meshes will return + an :class:`pyvista.UnstructuredGrid` object. + """ + if mesh_type != "unstructured": + grid = to_vtk_structured(pos=pos, fields=fields) + else: + grid = to_vtk_unstructured(pos=pos, fields=fields) + return grid + + +def vtk_export( + filename, pos, fields, mesh_type="unstructured" +): # pragma: no cover + """Export a field to vtk. + + Parameters + ---------- + filename : :class:`str` + Filename of the file to be saved, including the path. Note that an + ending (.vtr or .vtu) will be added to the name. + pos : :class:`list` + the position tuple, containing main direction and transversal + directions + fields : :class:`dict` or :class:`numpy.ndarray` + [Un]structured fields to be saved. + Either a single numpy array as returned by SRF, + or a dictionary of fields with theirs names as keys. + mesh_type : :class:`str`, optional + 'structured' / 'unstructured'. Default: structured + """ + if mesh_type != "unstructured": + return vtk_export_structured(filename=filename, pos=pos, fields=fields) + return vtk_export_unstructured(filename=filename, pos=pos, fields=fields) diff --git a/GSTools/source/src/gstools/tools/geometric.py b/GSTools/source/src/gstools/tools/geometric.py new file mode 100644 index 0000000000000000000000000000000000000000..5ba801278c4bd0bca265b3154440a8d19095be50 --- /dev/null +++ b/GSTools/source/src/gstools/tools/geometric.py @@ -0,0 +1,753 @@ +""" +GStools subpackage providing geometric tools. + +.. currentmodule:: gstools.tools.geometric + +The following functions are provided + +.. autosummary:: + set_angles + set_anis + no_of_angles + rotation_planes + givens_rotation + matrix_rotate + matrix_derotate + matrix_isotropify + matrix_anisotropify + matrix_isometrize + matrix_anisometrize + rotated_main_axes + generate_grid + generate_st_grid + format_struct_pos_dim + format_struct_pos_shape + format_unstruct_pos_shape + ang2dir + latlon2pos + pos2latlon + chordal_to_great_circle + great_circle_to_chordal +""" + +import numpy as np + +__all__ = [ + "set_angles", + "set_anis", + "no_of_angles", + "rotation_planes", + "givens_rotation", + "matrix_rotate", + "matrix_derotate", + "matrix_isotropify", + "matrix_anisotropify", + "matrix_isometrize", + "matrix_anisometrize", + "rotated_main_axes", + "generate_grid", + "generate_st_grid", + "format_struct_pos_dim", + "format_struct_pos_shape", + "format_unstruct_pos_shape", + "ang2dir", + "latlon2pos", + "pos2latlon", + "chordal_to_great_circle", +] + + +# Geometric functions ######################################################### + + +def set_angles(dim, angles): + """Set the angles for the given dimension. + + Parameters + ---------- + dim : :class:`int` + spatial dimension + angles : :class:`float` or :class:`list` + the angles of the SRF + + Returns + ------- + angles : :class:`float` + the angles fitting to the dimension + + Notes + ----- + If too few angles are given, they are filled up with `0`. + """ + out_angles = np.asarray(angles, dtype=np.double) + out_angles = np.atleast_1d(out_angles)[: no_of_angles(dim)] + # fill up the rotation angle array with zeros + out_angles = np.pad( + out_angles, + (0, no_of_angles(dim) - len(out_angles)), + "constant", + constant_values=0.0, + ) + return out_angles + + +def set_anis(dim, anis): + """Set the anisotropy ratios for the given dimension. + + Parameters + ---------- + dim : :class:`int` + spatial dimension + anis : :class:`list` of :class:`float` + the anisotropy of length scales along the transversal directions + + Returns + ------- + anis : :class:`list` of :class:`float` + the anisotropy of length scales fitting the dimensions + + Notes + ----- + If too few anisotropy ratios are given, they are filled up with `1`. + """ + out_anis = np.asarray(anis, dtype=np.double) + out_anis = np.atleast_1d(out_anis)[: dim - 1] + if len(out_anis) < dim - 1: + # fill up the anisotropies with ones, such that len()==dim-1 + out_anis = np.pad( + out_anis, + (dim - len(out_anis) - 1, 0), + "constant", + constant_values=1.0, + ) + return out_anis + + +def no_of_angles(dim): + """Calculate number of rotation angles depending on the dimension. + + Parameters + ---------- + dim : :class:`int` + spatial dimension + + Returns + ------- + :class:`int` + Number of angles. + """ + return (dim * (dim - 1)) // 2 + + +def rotation_planes(dim): + """Get all 2D sub-planes for rotation. + + Parameters + ---------- + dim : :class:`int` + spatial dimension + + Returns + ------- + :class:`list` of :class:`tuple` of :class:`int` + All 2D sub-planes for rotation. + """ + return [(i, j) for j in range(1, dim) for i in range(j)] + + +def givens_rotation(dim, plane, angle): + """Givens rotation matrix in arbitrary dimensions. + + Parameters + ---------- + dim : :class:`int` + spatial dimension + plane : :class:`list` of :class:`int` + the plane to rotate in, given by the indices of the two defining axes. + For example the xy plane is defined by `(0,1)` + angle : :class:`float` or :class:`list` + the rotation angle in the given plane + + Returns + ------- + :class:`numpy.ndarray` + Rotation matrix. + """ + result = np.eye(dim, dtype=np.double) + result[plane[0], plane[0]] = np.cos(angle) + result[plane[1], plane[1]] = np.cos(angle) + result[plane[0], plane[1]] = -np.sin(angle) + result[plane[1], plane[0]] = np.sin(angle) + return result + + +def matrix_rotate(dim, angles): + """Create a matrix to rotate points to the target coordinate-system. + + Parameters + ---------- + dim : :class:`int` + spatial dimension + angles : :class:`float` or :class:`list` + the rotation angles of the target coordinate-system + + Returns + ------- + :class:`numpy.ndarray` + Rotation matrix. + """ + angles = set_angles(dim, angles) + planes = rotation_planes(dim) + result = np.eye(dim, dtype=np.double) + for i, (angle, plane) in enumerate(zip(angles, planes)): + # angles have alternating signs to match tait-bryan + result = np.matmul( + givens_rotation(dim, plane, (-1) ** i * angle), result + ) + return result + + +def matrix_derotate(dim, angles): + """Create a matrix to derotate points to the initial coordinate-system. + + Parameters + ---------- + dim : :class:`int` + spatial dimension + angles : :class:`float` or :class:`list` + the rotation angles of the target coordinate-system + + Returns + ------- + :class:`numpy.ndarray` + Rotation matrix. + """ + # derotating by taking negative angles + angles = -set_angles(dim, angles) + planes = rotation_planes(dim) + result = np.eye(dim, dtype=np.double) + for i, (angle, plane) in enumerate(zip(angles, planes)): + # angles have alternating signs to match tait bryan + result = np.matmul( + result, givens_rotation(dim, plane, (-1) ** i * angle) + ) + return result + + +def matrix_isotropify(dim, anis): + """Create a stretching matrix to make things isotrope. + + Parameters + ---------- + dim : :class:`int` + spatial dimension + anis : :class:`list` of :class:`float` + the anisotropy of length scales along the transversal directions + + Returns + ------- + :class:`numpy.ndarray` + Stretching matrix. + """ + anis = set_anis(dim, anis) + return np.diag(np.concatenate(([1.0], 1.0 / anis))) + + +def matrix_anisotropify(dim, anis): + """Create a stretching matrix to make things anisotrope. + + Parameters + ---------- + dim : :class:`int` + spatial dimension + anis : :class:`list` of :class:`float` + the anisotropy of length scales along the transversal directions + + Returns + ------- + :class:`numpy.ndarray` + Stretching matrix. + """ + anis = set_anis(dim, anis) + return np.diag(np.concatenate(([1.0], anis))) + + +def matrix_isometrize(dim, angles, anis): + """Create a matrix to derotate points and make them isotrope. + + Parameters + ---------- + dim : :class:`int` + spatial dimension + angles : :class:`float` or :class:`list` + the rotation angles of the target coordinate-system + anis : :class:`list` of :class:`float` + the anisotropy of length scales along the transversal directions + + Returns + ------- + :class:`numpy.ndarray` + Transformation matrix. + """ + return np.matmul( + matrix_isotropify(dim, anis), matrix_derotate(dim, angles) + ) + + +def matrix_anisometrize(dim, angles, anis): + """Create a matrix to rotate points and make them anisotrope. + + Parameters + ---------- + dim : :class:`int` + spatial dimension + angles : :class:`float` or :class:`list` + the rotation angles of the target coordinate-system + anis : :class:`list` of :class:`float` + the anisotropy of length scales along the transversal directions + + Returns + ------- + :class:`numpy.ndarray` + Transformation matrix. + """ + return np.matmul( + matrix_rotate(dim, angles), matrix_anisotropify(dim, anis) + ) + + +def rotated_main_axes(dim, angles): + """Create list of the main axis defined by the given system rotations. + + Parameters + ---------- + dim : :class:`int` + spatial dimension + angles : :class:`float` or :class:`list` + the rotation angles of the target coordinate-system + + Returns + ------- + :class:`numpy.ndarray` + Main axes of the target coordinate-system. + """ + return matrix_rotate(dim, angles).T + + +# grid routines ############################################################### + + +def generate_grid(pos): + """ + Generate grid from a structured position tuple. + + Parameters + ---------- + pos : :class:`tuple` of :class:`numpy.ndarray` + The structured position tuple. + + Returns + ------- + :class:`numpy.ndarray` + Unstructured position tuple. + """ + return np.asarray( + np.meshgrid(*pos, indexing="ij"), dtype=np.double + ).reshape((len(pos), -1)) + + +def generate_st_grid(pos, time, mesh_type="unstructured"): + """ + Generate spatio-temporal grid from a position tuple and time array. + + Parameters + ---------- + pos : :class:`tuple` of :class:`numpy.ndarray` + The (un-)structured position tuple. + time : :any:`iterable` + The time array. + mesh_type : :class:`str`, optional + 'structured' / 'unstructured' + Default: `"unstructured"` + + Returns + ------- + :class:`numpy.ndarray` + Unstructured spatio-temporal point tuple. + + Notes + ----- + Time dimension will be the last one. + """ + time = np.asarray(time, dtype=np.double).reshape(-1) + if mesh_type != "unstructured": + pos = generate_grid(pos) + else: + pos = np.atleast_2d(np.asarray(pos, dtype=np.double)) + out = [np.repeat(p.reshape(-1), np.size(time)) for p in pos] + out.append(np.tile(time, np.size(pos[0]))) + return np.asarray(out, dtype=np.double) + + +# conversion ################################################################## + + +def format_struct_pos_dim(pos, dim): + """ + Format a structured position tuple with given dimension. + + Parameters + ---------- + pos : :any:`iterable` + Position tuple, containing main direction and transversal directions. + dim : :class:`int` + Spatial dimension. + + Raises + ------ + ValueError + When position tuple doesn't match the given dimension. + + Returns + ------- + pos : :class:`tuple` of :class:`numpy.ndarray` + The formatted structured position tuple. + shape : :class:`tuple` + Shape of the resulting field. + """ + if dim == 1: + pos = (np.asarray(pos, dtype=np.double).reshape(-1),) + elif len(pos) != dim: + raise ValueError("Formatting: position tuple doesn't match dimension.") + else: + pos = tuple(np.asarray(p, dtype=np.double).reshape(-1) for p in pos) + shape = tuple(len(p) for p in pos) + return pos, shape + + +def format_struct_pos_shape(pos, shape, check_stacked_shape=False): + """ + Format a structured position tuple with given shape. + + Shape could be stacked, when multiple fields are given. + + Parameters + ---------- + pos : :any:`iterable` + Position tuple, containing main direction and transversal directions. + shape : :class:`tuple` + Shape of the input field. + check_stacked_shape : :class:`bool`, optional + Whether to check if given shape comes from stacked fields. + Default: False. + + Raises + ------ + ValueError + When position tuple doesn't match the given dimension. + + Returns + ------- + pos : :class:`tuple` of :class:`numpy.ndarray` + The formatted structured position tuple. + shape : :class:`tuple` + Shape of the resulting field. + dim : :class:`int` + Spatial dimension. + """ + # some help from the given shape + shape_size = np.prod(shape) + stacked_shape_size = np.prod(shape[1:]) + wrong_shape = False + # now we try to be smart + try: + # if this works we have either: + # - a 1D array + # - nD array where all axes have same length (corner case) + check_pos = np.array(pos, dtype=np.double, ndmin=2) + except ValueError: + # if it doesn't work, we have a tuple of differently sized axes (easy) + dim = len(pos) + pos, pos_shape = format_struct_pos_dim(pos, dim) + # determine if we have a stacked field if wanted + if check_stacked_shape and stacked_shape_size == np.prod(pos_shape): + shape = (shape[0],) + pos_shape + # check if we have a single field with matching size + elif shape_size == np.prod(pos_shape): + shape = (1,) + pos_shape if check_stacked_shape else pos_shape + # if nothing works, we raise an error + else: + wrong_shape = True + else: + struct_size = np.prod([p.size for p in check_pos]) + # case: 1D unstacked + if check_pos.size == shape_size: + dim = 1 + pos, pos_shape = format_struct_pos_dim(check_pos, dim) + shape = (1,) + pos_shape if check_stacked_shape else pos_shape + # case: 1D and stacked + elif check_pos.size == stacked_shape_size: + dim = 1 + pos, pos_shape = format_struct_pos_dim(check_pos, dim) + cnt = shape[0] + shape = (cnt,) + pos_shape + wrong_shape = not check_stacked_shape + # case: nD unstacked + elif struct_size == shape_size: + dim = len(check_pos) + pos, pos_shape = format_struct_pos_dim(pos, dim) + shape = (1,) + pos_shape if check_stacked_shape else pos_shape + # case: nD and stacked + elif struct_size == stacked_shape_size: + dim = len(check_pos) + pos, pos_shape = format_struct_pos_dim(pos, dim) + cnt = shape[0] + shape = (cnt,) + pos_shape + wrong_shape = not check_stacked_shape + # if nothing works, we raise an error + else: + wrong_shape = True + + # if shape was wrong at one point we raise an error + if wrong_shape: + raise ValueError("Formatting: position tuple doesn't match dimension.") + + return pos, shape, dim + + +def format_unstruct_pos_shape(pos, shape, check_stacked_shape=False): + """ + Format an unstructured position tuple with given shape. + + Shape could be stacked, when multiple fields were given. + + Parameters + ---------- + pos : :any:`iterable` + Position tuple, containing point coordinates. + shape : :class:`tuple` + Shape of the input field. + check_stacked_shape : :class:`bool`, optional + Whether to check if given shape comes from stacked fields. + Default: False. + + Raises + ------ + ValueError + When position tuple doesn't match the given dimension. + + Returns + ------- + pos : :class:`tuple` of :class:`numpy.ndarray` + The formatted structured position tuple. + shape : :class:`tuple` + Shape of the resulting field. + dim : :class:`int` + Spatial dimension. + """ + # some help from the given shape + shape_size = np.prod(shape) + stacked_shape_size = np.prod(shape[1:]) + wrong_shape = False + # now we try to be smart + pre_len = len(np.atleast_1d(pos)) + # care about 1D: pos can be given as 1D array here -> convert to 2D array + pos = np.atleast_2d(np.asarray(pos, dtype=np.double)) + post_len = len(pos) + # first array dimension should be spatial dimension (1D is special case) + dim = post_len if pre_len == post_len else 1 + pnt_cnt = pos[0].size + # case: 1D unstacked + if dim == 1 and pos.size == shape_size: + shape = (1, pos.size) if check_stacked_shape else (pos.size,) + # case: 1D and stacked + elif dim == 1 and pos.size == stacked_shape_size: + shape = (shape[0], pos.size) + wrong_shape = not check_stacked_shape + # case: nD unstacked + elif pnt_cnt == shape_size: + shape = (1, pnt_cnt) if check_stacked_shape else pnt_cnt + # case: nD and stacked + elif pnt_cnt == stacked_shape_size: + shape = (shape[0], pnt_cnt) + wrong_shape = not check_stacked_shape + # if nothing works, we raise an error + else: + wrong_shape = True + + # if shape was wrong at one point we raise an error + if wrong_shape: + raise ValueError("Formatting: position tuple doesn't match dimension.") + + pos = pos.reshape((dim, -1)) + + return pos, shape, dim + + +def ang2dir(angles, dtype=np.double, dim=None): + """Convert n-D spherical coordinates to Euclidean direction vectors. + + Parameters + ---------- + angles : :class:`list` of :class:`numpy.ndarray` + spherical coordinates given as angles. + dtype : data-type, optional + The desired data-type for the array. + If not given, then the type will be determined as the minimum type + required to hold the objects in the sequence. Default: None + dim : :class:`int`, optional + Cut of information above the given dimension. + Otherwise, dimension is determined by number of angles + Default: None + + Returns + ------- + :class:`numpy.ndarray` + the array of direction vectors + """ + pre_dim = np.asanyarray(angles).ndim + angles = np.atleast_2d(np.asarray(angles, dtype=dtype)) + if len(angles.shape) > 2: + raise ValueError(f"Can't interpret angles array {angles}") + dim = angles.shape[1] + 1 if dim is None else dim + if dim == 2 and angles.shape[0] == 1 and pre_dim < 2: + # fix for 2D where only one angle per direction is given + angles = angles.T # can't be interpreted if dim=None is given + if dim != angles.shape[1] + 1 or dim == 1: + raise ValueError(f"Wrong dim. ({dim}) for angles {angles}") + vec = np.empty((angles.shape[0], dim), dtype=dtype) + vec[:, 0] = np.prod(np.sin(angles), axis=1) + for i in range(1, dim): + vec[:, i] = np.prod(np.sin(angles[:, i:]), axis=1) # empty prod = 1 + vec[:, i] *= np.cos(angles[:, (i - 1)]) + if dim in [2, 3]: + vec[:, [0, 1]] = vec[:, [1, 0]] # to match convention in 2D and 3D + return vec + + +def latlon2pos( + latlon, radius=1.0, dtype=np.double, temporal=False, time_scale=1.0 +): + """Convert lat-lon geo coordinates to 3D position tuple. + + Parameters + ---------- + latlon : :class:`list` of :class:`numpy.ndarray` + latitude and longitude given in degrees. + May includes an appended time axis if `time=True`. + radius : :class:`float`, optional + Sphere radius. Default: `1.0` + dtype : data-type, optional + The desired data-type for the array. + If not given, then the type will be determined as the minimum type + required to hold the objects in the sequence. Default: None + temporal : :class:`bool`, optional + Whether latlon includes an appended time axis. + Default: False + time_scale : :class:`float`, optional + Scaling factor (e.g. anisotropy) for the time axis. + Default: `1.0` + + Returns + ------- + :class:`numpy.ndarray` + the 3D position array + """ + latlon = np.asarray(latlon, dtype=dtype).reshape( + (3 if temporal else 2, -1) + ) + lat, lon = np.deg2rad(latlon[:2]) + pos_tuple = ( + radius * np.cos(lat) * np.cos(lon), + radius * np.cos(lat) * np.sin(lon), + radius * np.sin(lat) * np.ones_like(lon), + ) + if temporal: + return np.array(pos_tuple + (latlon[2] / time_scale,), dtype=dtype) + return np.array(pos_tuple, dtype=dtype) + + +def pos2latlon( + pos, radius=1.0, dtype=np.double, temporal=False, time_scale=1.0 +): + """Convert 3D position tuple from sphere to lat-lon geo coordinates. + + Parameters + ---------- + pos : :class:`list` of :class:`numpy.ndarray` + The position tuple containing points on a unit-sphere. + May includes an appended time axis if `time=True`. + radius : :class:`float`, optional + Sphere radius. Default: `1.0` + dtype : data-type, optional + The desired data-type for the array. + If not given, then the type will be determined as the minimum type + required to hold the objects in the sequence. Default: None + temporal : :class:`bool`, optional + Whether latlon includes an appended time axis. + Default: False + time_scale : :class:`float`, optional + Scaling factor (e.g. anisotropy) for the time axis. + Default: `1.0` + + Returns + ------- + :class:`numpy.ndarray` + the 3D position array + """ + pos = np.asarray(pos, dtype=dtype).reshape((4 if temporal else 3, -1)) + # prevent numerical errors in arcsin + lat = np.arcsin(np.maximum(np.minimum(pos[2] / radius, 1.0), -1.0)) + lon = np.arctan2(pos[1], pos[0]) + latlon = np.rad2deg((lat, lon), dtype=dtype) + if temporal: + return np.array( + (latlon[0], latlon[1], pos[3] * time_scale), dtype=dtype + ) + return latlon + + +def chordal_to_great_circle(dist, radius=1.0): + """ + Calculate great circle distance corresponding to given chordal distance. + + Parameters + ---------- + dist : array_like + Chordal distance of two points on the sphere. + radius : :class:`float`, optional + Sphere radius. Default: `1.0` + + Returns + ------- + :class:`numpy.ndarray` + Great circle distance corresponding to given chordal distance. + + Notes + ----- + If given values are not in [0, 2 * radius], they will be truncated. + """ + diameter = 2 * radius + return diameter * np.arcsin( + np.maximum(np.minimum(np.divide(dist, diameter), 1), 0) + ) + + +def great_circle_to_chordal(dist, radius=1.0): + """ + Calculate chordal distance corresponding to given great circle distance. + + Parameters + ---------- + dist : array_like + Great circle distance of two points on the sphere. + radius : :class:`float`, optional + Sphere radius. Default: `1.0` + + Returns + ------- + :class:`numpy.ndarray` + Chordal distance corresponding to given great circle distance. + """ + diameter = 2 * radius + return diameter * np.sin(np.divide(dist, diameter)) diff --git a/GSTools/source/src/gstools/tools/misc.py b/GSTools/source/src/gstools/tools/misc.py new file mode 100644 index 0000000000000000000000000000000000000000..3fe44678e071d87326ad6732483f9dd3baefff27 --- /dev/null +++ b/GSTools/source/src/gstools/tools/misc.py @@ -0,0 +1,142 @@ +""" +GStools subpackage providing miscellaneous tools. + +.. currentmodule:: gstools.tools.misc + +The following functions are provided + +.. autosummary:: + get_fig_ax + list_format + eval_func +""" + +import numpy as np + +from gstools.tools.geometric import format_struct_pos_dim, generate_grid + +__all__ = ["get_fig_ax", "list_format", "eval_func"] + + +def get_fig_ax(fig=None, ax=None, ax_name="rectilinear"): # pragma: no cover + """ + Get correct matplotlib figure and axes. + + Parameters + ---------- + fig : figure or :any:`None` + desired figure. + ax : axis or :any:`None` + desired axis. + ax_name : :class:`str`, optional + Axis type name. The default is "rectilinear". + + Returns + ------- + fig : figure + desired figure. + ax : axis + desired axis. + """ + try: + from matplotlib import pyplot as plt + except ImportError as exc: + raise ImportError("Plotting: Matplotlib not installed.") from exc + + if fig is None and ax is None: + fig = plt.figure() + ax = fig.add_subplot(111, projection=ax_name) + elif ax is None: + ax = fig.add_subplot(111, projection=ax_name) + elif fig is None: + fig = ax.get_figure() + assert ax.name == ax_name + else: + assert ax.name == ax_name + assert ax.get_figure() == fig + return fig, ax + + +def list_format(lst, prec): # pragma: no cover + """Format a list of floats.""" + return f"[{', '.join(f'{float(x):.{prec}}' for x in lst)}]" + + +def eval_func( + func_val, + pos, + dim, + mesh_type="unstructured", + value_type="scalar", + broadcast=False, +): + """ + Evaluate a function on a mesh. + + Parameters + ---------- + func_val : :any:`callable` or :class:`float` or :any:`None` + Function to be called or single value to be filled. + Should have the signature f(x, [y, z, ...]) in case of callable. + In case of a float, the field will be filled with a single value and + in case of None, this value will be set to 0. + pos : :class:`list` + The position tuple, containing main direction and transversal + directions (x, [y, z, ...]). + dim : :class:`int` + The spatial dimension. + mesh_type : :class:`str`, optional + 'structured' / 'unstructured' + Default: 'unstructured' + value_type : :class:`str`, optional + Value type of the field. Either "scalar" or "vector". + The default is "scalar". + broadcast : :class:`bool`, optional + Whether to return a single value, if a single value was given. + Default: False + + Returns + ------- + :class:`numpy.ndarray` + Function values at the given points. + """ + # care about scalar inputs + func_val = 0 if func_val is None else func_val + if broadcast and not callable(func_val) and np.size(func_val) == 1: + return np.asarray(func_val, dtype=np.double).item() + if not callable(func_val): + func_val = _func_from_single_val(func_val, dim, value_type=value_type) + # care about mesh and function call + if mesh_type != "unstructured": + pos, shape = format_struct_pos_dim(pos, dim) + pos = generate_grid(pos) + else: + pos = np.asarray(pos, dtype=np.double).reshape(dim, -1) + shape = np.shape(pos[0]) + # prepend dimension if we have a vector field + if value_type == "vector": + shape = (dim,) + shape + return np.reshape(func_val(*pos), shape) + + +def _func_from_single_val(value, dim=None, value_type="scalar"): + # care about broadcasting vector values for each dim + v_d = dim if value_type == "vector" else 1 # value dim + if v_d is None: # pragma: no cover + raise ValueError("_func_from_single_val: dim needed for vector value.") + value = np.asarray(value, dtype=np.double).ravel()[:v_d] + # fill up vector valued output to dimension with last value + value = np.pad( + value, (0, v_d - len(value)), "constant", constant_values=value[-1] + ) + + def _f(*pos): + # zip uses shortest len of iterables given (correct for scalar value) + return np.concatenate( + [ + np.full_like(p, val, dtype=np.double) + for p, val in zip(pos, value) + ] + ) + + return _f diff --git a/GSTools/source/src/gstools/tools/special.py b/GSTools/source/src/gstools/tools/special.py new file mode 100644 index 0000000000000000000000000000000000000000..5e6374ac42d2ca86675a190c52b432c9ccaa6801 --- /dev/null +++ b/GSTools/source/src/gstools/tools/special.py @@ -0,0 +1,256 @@ +""" +GStools subpackage providing special functions. + +.. currentmodule:: gstools.tools.special + +The following functions are provided + +.. autosummary:: + inc_gamma + inc_gamma_low + exp_int + inc_beta + tplstable_cor + tpl_exp_spec_dens + tpl_gau_spec_dens +""" + +import numpy as np +from scipy import special as sps + +__all__ = [ + "confidence_scaling", + "inc_gamma", + "inc_gamma_low", + "exp_int", + "inc_beta", + "tplstable_cor", + "tpl_exp_spec_dens", + "tpl_gau_spec_dens", +] + + +# special functions ########################################################### + + +def confidence_scaling(per=0.95): + """ + Scaling of standard deviation to get the desired confidence interval. + + Parameters + ---------- + per : :class:`float`, optional + Confidence level. The default is 0.95. + + Returns + ------- + :class:`float` + Scale to multiply the standard deviation with. + """ + return np.sqrt(2) * sps.erfinv(per) + + +def inc_gamma(s, x): + r"""Calculate the (upper) incomplete gamma function. + + Given by: :math:`\Gamma(s,x) = \int_x^{\infty} t^{s-1}\,e^{-t}\,{\rm d}t` + + Parameters + ---------- + s : :class:`float` + exponent in the integral + x : :class:`numpy.ndarray` + input values + """ + if np.isclose(s, 0): + return sps.exp1(x) + if np.isclose(s, np.around(s)) and s < -0.5: + return x**s * sps.expn(int(1 - np.around(s)), x) + if s < 0: + return (inc_gamma(s + 1, x) - x**s * np.exp(-x)) / s + return sps.gamma(s) * sps.gammaincc(s, x) + + +def inc_gamma_low(s, x): + r"""Calculate the lower incomplete gamma function. + + Given by: :math:`\gamma(s,x) = \int_0^x t^{s-1}\,e^{-t}\,{\rm d}t` + + Parameters + ---------- + s : :class:`float` + exponent in the integral + x : :class:`numpy.ndarray` + input values + """ + if np.isclose(s, np.around(s)) and s < 0.5: + return np.full_like(x, np.inf, dtype=np.double) + if s < 0: + return (inc_gamma_low(s + 1, x) + x**s * np.exp(-x)) / s + return sps.gamma(s) * sps.gammainc(s, x) + + +def exp_int(s, x): + r"""Calculate the exponential integral :math:`E_s(x)`. + + Given by: :math:`E_s(x) = \int_1^\infty \frac{e^{-xt}}{t^s}\,\mathrm dt` + + Parameters + ---------- + s : :class:`float` + exponent in the integral (should be > -100) + x : :class:`numpy.ndarray` + input values + """ + if np.isclose(s, 1): + return sps.exp1(x) + if np.isclose(s, np.around(s)) and s > -0.5: + return sps.expn(int(np.around(s)), x) + x = np.asarray(x, dtype=np.double) + x_neg = x < 0 + x = np.abs(x) + x_compare = x ** min((10, max(((1 - s), 1)))) + res = np.empty_like(x) + # use asymptotic behavior for zeros + x_zero = np.isclose(x_compare, 0, atol=1e-20) + x_inf = x > max(30, -s / 2) # function is like exp(-x)*(1/x + s/x^2) + x_fin = np.logical_not(np.logical_or(x_zero, x_inf)) + x_fin_pos = np.logical_and(x_fin, np.logical_not(x_neg)) + if s > 1.0: # limit at x=+0 + res[x_zero] = 1.0 / (s - 1.0) + else: + res[x_zero] = np.inf + res[x_inf] = np.exp(-x[x_inf]) * (x[x_inf] ** -1 - s * x[x_inf] ** -2) + res[x_fin_pos] = inc_gamma(1 - s, x[x_fin_pos]) * x[x_fin_pos] ** (s - 1) + res[x_neg] = np.nan # nan for x < 0 + return res + + +def inc_beta(a, b, x): + r"""Calculate the incomplete Beta function. + + Given by: :math:`B(a,b;\,x) = \int_0^x t^{a-1}\,(1-t)^{b-1}\,dt` + + Parameters + ---------- + a : :class:`float` + first exponent in the integral + b : :class:`float` + second exponent in the integral + x : :class:`numpy.ndarray` + input values + """ + return sps.betainc(a, b, x) * sps.beta(a, b) + + +def tplstable_cor(r, len_scale, hurst, alpha): + r"""Calculate the correlation function of the TPLStable model. + + Given by the following correlation function: + + .. math:: + \rho(r) = + \frac{2H}{\alpha} \cdot + E_{1+\frac{2H}{\alpha}} + \left(\left(\frac{r}{\ell}\right)^{\alpha} \right) + + + Parameters + ---------- + r : :class:`numpy.ndarray` + input values + len_scale : :class:`float` + length-scale of the model. + hurst : :class:`float` + Hurst coefficient of the power law. + alpha : :class:`float`, optional + Shape parameter of the stable model. + """ + r = np.asarray(np.abs(r / len_scale), dtype=np.double) + r[np.isclose(r, 0)] = 0 # hack to prevent numerical errors + res = np.ones_like(r) + res[r > 0] = (2 * hurst / alpha) * exp_int( + 1 + 2 * hurst / alpha, (r[r > 0]) ** alpha + ) + return res + + +def tpl_exp_spec_dens(k, dim, len_scale, hurst, len_low=0.0): + r""" + Spectral density of the TPLExponential covariance model. + + Parameters + ---------- + k : :class:`float` + Radius of the phase: :math:`k=\left\Vert\mathbf{k}\right\Vert` + dim : :class:`int` + Dimension of the model. + len_scale : :class:`float` + Length scale of the model. + hurst : :class:`float` + Hurst coefficient of the power law. + len_low : :class:`float`, optional + The lower length scale truncation of the model. + Default: 0.0 + + Returns + ------- + :class:`float` + spectral density of the TPLExponential model + """ + if np.isclose(len_low, 0.0): + k = np.asarray(k, dtype=np.double) + z = (k * len_scale) ** 2 + a = hurst + dim / 2.0 + b = hurst + 0.5 + c = hurst + dim / 2.0 + 1.0 + d = dim / 2.0 + 0.5 + fac = len_scale**dim * hurst * sps.gamma(d) / (np.pi**d * a) + return fac / (1.0 + z) ** a * sps.hyp2f1(a, b, c, z / (1.0 + z)) + fac_up = (len_scale + len_low) ** (2 * hurst) + spec_up = tpl_exp_spec_dens(k, dim, len_scale + len_low, hurst) + fac_low = len_low ** (2 * hurst) + spec_low = tpl_exp_spec_dens(k, dim, len_low, hurst) + return (fac_up * spec_up - fac_low * spec_low) / (fac_up - fac_low) + + +def tpl_gau_spec_dens(k, dim, len_scale, hurst, len_low=0.0): + r""" + Spectral density of the TPLGaussian covariance model. + + Parameters + ---------- + k : :class:`float` + Radius of the phase: :math:`k=\left\Vert\mathbf{k}\right\Vert` + dim : :class:`int` + Dimension of the model. + len_scale : :class:`float` + Length scale of the model. + hurst : :class:`float` + Hurst coefficient of the power law. + len_low : :class:`float`, optional + The lower length scale truncation of the model. + Default: 0.0 + + Returns + ------- + :class:`float` + spectral density of the TPLExponential model + """ + if np.isclose(len_low, 0.0): + k = np.asarray(k, dtype=np.double) + z = np.array((k * len_scale / 2.0) ** 2) + res = np.empty_like(z) + z_gz = z > 0.1 # greater zero + z_nz = np.logical_not(z_gz) # near zero + a = hurst + dim / 2.0 + fac = (len_scale / 2.0) ** dim * hurst / np.pi ** (dim / 2.0) + res[z_gz] = fac * inc_gamma_low(a, z[z_gz]) / z[z_gz] ** a + # first order approximation for z near zero + res[z_nz] = fac * (1.0 / a - z[z_nz] / (a + 1.0)) + return res + fac_up = (len_scale + len_low) ** (2 * hurst) + spec_up = tpl_gau_spec_dens(k, dim, len_scale + len_low, hurst) + fac_low = len_low ** (2 * hurst) + spec_low = tpl_gau_spec_dens(k, dim, len_low, hurst) + return (fac_up * spec_up - fac_low * spec_low) / (fac_up - fac_low) diff --git a/GSTools/source/src/gstools/transform/__init__.py b/GSTools/source/src/gstools/transform/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b2e6ce4f245176ac8b894481babc4e4c4fc27a52 --- /dev/null +++ b/GSTools/source/src/gstools/transform/__init__.py @@ -0,0 +1,93 @@ +""" +GStools subpackage providing transformations to post-process normal fields. + +.. currentmodule:: gstools.transform + +Wrapper +^^^^^^^ + +.. autosummary:: + :toctree: + + apply + +Field Transformations +^^^^^^^^^^^^^^^^^^^^^ + +.. autosummary:: + :toctree: + + binary + discrete + boxcox + zinnharvey + normal_force_moments + normal_to_lognormal + normal_to_uniform + normal_to_arcsin + normal_to_uquad + apply_function + +Array Transformations +^^^^^^^^^^^^^^^^^^^^^ + +.. autosummary:: + :toctree: + + array_discrete + array_boxcox + array_zinnharvey + array_force_moments + array_to_lognormal + array_to_uniform + array_to_arcsin + array_to_uquad + +---- +""" + +from gstools.transform.array import ( + array_boxcox, + array_discrete, + array_force_moments, + array_to_arcsin, + array_to_lognormal, + array_to_uniform, + array_to_uquad, + array_zinnharvey, +) +from gstools.transform.field import ( + apply, + apply_function, + binary, + boxcox, + discrete, + normal_force_moments, + normal_to_arcsin, + normal_to_lognormal, + normal_to_uniform, + normal_to_uquad, + zinnharvey, +) + +__all__ = [ + "apply", + "apply_function", + "binary", + "discrete", + "boxcox", + "zinnharvey", + "normal_force_moments", + "normal_to_lognormal", + "normal_to_uniform", + "normal_to_arcsin", + "normal_to_uquad", + "array_discrete", + "array_boxcox", + "array_zinnharvey", + "array_force_moments", + "array_to_lognormal", + "array_to_uniform", + "array_to_arcsin", + "array_to_uquad", +] diff --git a/GSTools/source/src/gstools/transform/array.py b/GSTools/source/src/gstools/transform/array.py new file mode 100644 index 0000000000000000000000000000000000000000..16fd07f7c9b59c9428a70f2ba159663206bd68a6 --- /dev/null +++ b/GSTools/source/src/gstools/transform/array.py @@ -0,0 +1,359 @@ +""" +GStools subpackage providing array transformations. + +.. currentmodule:: gstools.transform.array + +The following functions are provided + +Transformations +^^^^^^^^^^^^^^^ + +.. autosummary:: + array_discrete + array_boxcox + array_zinnharvey + array_force_moments + array_to_lognormal + array_to_uniform + array_to_arcsin + array_to_uquad +""" + +from warnings import warn + +import numpy as np +from scipy.special import erf, erfinv + +__all__ = [ + "array_discrete", + "array_boxcox", + "array_zinnharvey", + "array_force_moments", + "array_to_lognormal", + "array_to_uniform", + "array_to_arcsin", + "array_to_uquad", +] + + +def array_discrete( + field, values, thresholds="arithmetic", mean=None, var=None +): + """ + Discrete transformation. + + After this transformation, the field has only `len(values)` discrete + values. + + Parameters + ---------- + field : :class:`numpy.ndarray` + Normal distributed values. + values : :any:`numpy.ndarray` + The discrete values the field will take + thresholds : :class:`str` or :any:`numpy.ndarray`, optional + the thresholds, where the value classes are separated + possible values are: + * "arithmetic": the mean of the 2 neighbouring values + * "equal": divide the field into equal parts + * an array of explicitly given thresholds + Default: "arithmetic" + mean : :class:`float`or :any:`None` + Mean of the field for "equal" thresholds. Default: np.mean(field) + var : :class:`float`or :any:`None` + Variance of the field for "equal" thresholds. Default: np.var(field) + + Returns + ------- + :class:`numpy.ndarray` + Transformed field. + """ + field = np.asarray(field) + if thresholds == "arithmetic": + # just in case, sort the values + values = np.sort(values) + thresholds = (values[1:] + values[:-1]) / 2 + elif thresholds == "equal": + mean = np.mean(field) if mean is None else float(mean) + var = np.var(field) if var is None else float(var) + values = np.asarray(values) + n = len(values) + p = np.arange(1, n) / n # n-1 equal subdivisions of [0, 1] + rescale = np.sqrt(var * 2) + # use quantile of the normal distribution to get equal ratios + thresholds = mean + rescale * erfinv(2 * p - 1) + else: + if len(values) != len(thresholds) + 1: + raise ValueError( + "discrete transformation: len(values) != len(thresholds) + 1" + ) + values = np.asarray(values) + thresholds = np.asarray(thresholds) + # check thresholds + if not np.all(thresholds[:-1] < thresholds[1:]): + raise ValueError( + "discrete transformation: thresholds need to be ascending" + ) + # use a separate result so the intermediate results are not affected + result = np.empty_like(field) + # handle edge cases + result[field <= thresholds[0]] = values[0] + result[field > thresholds[-1]] = values[-1] + for i, value in enumerate(values[1:-1]): + result[ + np.logical_and(thresholds[i] < field, field <= thresholds[i + 1]) + ] = value + return result + + +def array_boxcox(field, lmbda=1, shift=0): + """ + (Inverse) Box-Cox transformation to denormalize data. + + After this transformation, the again Box-Cox transformed field is normal + distributed. + + See: https://en.wikipedia.org/wiki/Power_transform#Box%E2%80%93Cox_transformation + + Parameters + ---------- + field : :class:`numpy.ndarray` + Normal distributed values. + lmbda : :class:`float`, optional + The lambda parameter of the Box-Cox transformation. + For ``lmbda=0`` one obtains the log-normal transformation. + Default: ``1`` + shift : :class:`float`, optional + The shift parameter from the two-parametric Box-Cox transformation. + The field will be shifted by that value before transformation. + Default: ``0`` + """ + field = np.asarray(field) + result = field + shift + if np.isclose(lmbda, 0): + return array_to_lognormal(result) + if np.min(result) < -1 / lmbda: + warn("Box-Cox: Some values will be cut off!") + return (np.maximum(lmbda * result + 1, 0)) ** (1 / lmbda) + + +def array_zinnharvey(field, conn="high", mean=None, var=None): + """ + Zinn and Harvey transformation to connect low or high values. + + Parameters + ---------- + field : :class:`numpy.ndarray` + Normal distributed values. + conn : :class:`str`, optional + Desired connectivity. Either "low" or "high". + Default: "high" + mean : :class:`float` or :any:`None`, optional + Mean of the given field. If None is given, the mean will be calculated. + Default: :any:`None` + var : :class:`float` or :any:`None`, optional + Variance of the given field. + If None is given, the variance will be calculated. + Default: :any:`None` + + Returns + ------- + :class:`numpy.ndarray` + Transformed field. + """ + field = np.asarray(field) + mean = np.mean(field) if mean is None else float(mean) + var = np.var(field) if var is None else float(var) + result = np.abs((field - mean) / np.sqrt(var)) + result = np.sqrt(2) * erfinv(2 * erf(result / np.sqrt(2)) - 1) + if conn == "high": + result = -result + return result * np.sqrt(var) + mean + + +def array_force_moments(field, mean=0, var=1): + """ + Force moments of a normal distributed field. + + Parameters + ---------- + field : :class:`numpy.ndarray` + Normal distributed values. + mean : :class:`float`, optional + Desired mean of the field. + Default: 0 + var : :class:`float` or :any:`None`, optional + Desired variance of the field. + Default: 1 + + Returns + ------- + :class:`numpy.ndarray` + Transformed field. + """ + field = np.asarray(field) + var_in = np.var(field) + mean_in = np.mean(field) + rescale = np.sqrt(var / var_in) + return rescale * (field - mean_in) + mean + + +def array_to_lognormal(field): + """ + Transform normal distribution to log-normal distribution. + + Parameters + ---------- + field : :class:`numpy.ndarray` + Normal distributed values. + + Returns + ------- + :class:`numpy.ndarray` + Transformed field. + """ + return np.exp(field) + + +def array_to_uniform(field, mean=None, var=None, low=0.0, high=1.0): + """ + Transform normal distribution to uniform distribution on [low, high]. + + Parameters + ---------- + field : :class:`numpy.ndarray` + Normal distributed values. + mean : :class:`float` or :any:`None`, optional + Mean of the given field. If None is given, the mean will be calculated. + Default: :any:`None` + var : :class:`float` or :any:`None`, optional + Variance of the given field. + If None is given, the variance will be calculated. + Default: :any:`None` + low : :class:`float`, optional + Lower bound for the uniform distribution. + Default: 0.0 + high : :class:`float`, optional + Upper bound for the uniform distribution. + Default: 1.0 + + Returns + ------- + :class:`numpy.ndarray` + Transformed field. + """ + field = np.asarray(field) + mean = np.mean(field) if mean is None else float(mean) + var = np.var(field) if var is None else float(var) + return ( + 0.5 * (1 + erf((field - mean) / np.sqrt(2 * var))) * (high - low) + low + ) + + +def array_to_arcsin(field, mean=None, var=None, a=None, b=None): + """ + Transform normal distribution to arcsin distribution. + + See: https://en.wikipedia.org/wiki/Arcsine_distribution + + Parameters + ---------- + field : :class:`numpy.ndarray` + Normal distributed values. + mean : :class:`float` or :any:`None`, optional + Mean of the given field. If None is given, the mean will be calculated. + Default: :any:`None` + var : :class:`float` or :any:`None`, optional + Variance of the given field. + If None is given, the mean will be calculated. + Default: :any:`None` + a : :class:`float`, optional + Parameter a of the arcsin distribution (lower bound). + Default: keep mean and variance + b : :class:`float`, optional + Parameter b of the arcsin distribution (upper bound). + Default: keep mean and variance + + Returns + ------- + :class:`numpy.ndarray` + Transformed field. + """ + field = np.asarray(field) + mean = np.mean(field) if mean is None else float(mean) + var = np.var(field) if var is None else float(var) + a = mean - np.sqrt(2.0 * var) if a is None else float(a) + b = mean + np.sqrt(2.0 * var) if b is None else float(b) + return _uniform_to_arcsin(array_to_uniform(field, mean, var), a, b) + + +def array_to_uquad(field, mean=None, var=None, a=None, b=None): + """ + Transform normal distribution to U-quadratic distribution. + + See: https://en.wikipedia.org/wiki/U-quadratic_distribution + + Parameters + ---------- + field : :class:`numpy.ndarray` + Normal distributed values. + mean : :class:`float` or :any:`None`, optional + Mean of the given field. If None is given, the mean will be calculated. + Default: :any:`None` + var : :class:`float` or :any:`None`, optional + Variance of the given field. + If None is given, the variance will be calculated. + Default: :any:`None` + a : :class:`float`, optional + Parameter a of the U-quadratic distribution (lower bound). + Default: keep mean and variance + b : :class:`float`, optional + Parameter b of the U-quadratic distribution (upper bound). + Default: keep mean and variance + + Returns + ------- + :class:`numpy.ndarray` + Transformed field. + """ + field = np.asarray(field) + mean = np.mean(field) if mean is None else float(mean) + var = np.var(field) if var is None else float(var) + a = mean - np.sqrt(5.0 / 3.0 * var) if a is None else float(a) + b = mean + np.sqrt(5.0 / 3.0 * var) if b is None else float(b) + return _uniform_to_uquad(array_to_uniform(field, mean, var), a, b) + + +def _uniform_to_arcsin(field, a=0, b=1): + """ + PPF of your desired distribution. + + The PPF is the inverse of the CDF and is used to sample a distribution + from uniform distributed values on [0, 1] + + in this case: the arcsin distribution + See: https://en.wikipedia.org/wiki/Arcsine_distribution + """ + field = np.asarray(field) + return (b - a) * np.sin(np.pi * 0.5 * field) ** 2 + a + + +def _uniform_to_uquad(field, a=0, b=1): + """ + PPF of your desired distribution. + + The PPF is the inverse of the CDF and is used to sample a distribution + from uniform distributed values on [0, 1] + + in this case: the U-quadratic distribution + See: https://en.wikipedia.org/wiki/U-quadratic_distribution + """ + field = np.asarray(field) + al = 12 / (b - a) ** 3 + be = (a + b) / 2 + ga = (a - b) ** 3 / 8 + y_raw = 3 * field / al + ga + result = np.zeros_like(y_raw) + result[y_raw > 0] = y_raw[y_raw > 0] ** (1 / 3) + result[y_raw < 0] = -((-y_raw[y_raw < 0]) ** (1 / 3)) + return result + be diff --git a/GSTools/source/src/gstools/transform/field.py b/GSTools/source/src/gstools/transform/field.py new file mode 100644 index 0000000000000000000000000000000000000000..659d17255729dc20eb4769ac3ae49732603cfd27 --- /dev/null +++ b/GSTools/source/src/gstools/transform/field.py @@ -0,0 +1,724 @@ +""" +GStools subpackage providing field transformations. + +.. currentmodule:: gstools.transform.field + +The following functions are provided + +Wrapper +^^^^^^^ + +.. autosummary:: + apply + +Transformations +^^^^^^^^^^^^^^^ + +.. autosummary:: + apply_function + binary + discrete + boxcox + zinnharvey + normal_force_moments + normal_to_lognormal + normal_to_uniform + normal_to_arcsin + normal_to_uquad +""" + +import numpy as np + +from gstools.normalizer import ( + Normalizer, + apply_mean_norm_trend, + remove_trend_norm_mean, +) +from gstools.transform.array import ( + array_boxcox, + array_discrete, + array_force_moments, + array_to_arcsin, + array_to_lognormal, + array_to_uniform, + array_to_uquad, + array_zinnharvey, +) + +__all__ = [ + "apply", + "apply_function", + "binary", + "discrete", + "boxcox", + "zinnharvey", + "normal_force_moments", + "normal_to_lognormal", + "normal_to_uniform", + "normal_to_arcsin", + "normal_to_uquad", +] + + +def _pre_process(fld, data, keep_mean): + return remove_trend_norm_mean( + pos=fld.pos, + field=data, + mean=None if keep_mean else fld.mean, + normalizer=fld.normalizer, + trend=fld.trend, + mesh_type=fld.mesh_type, + value_type=fld.value_type, + check_shape=False, + ) + + +def _post_process(fld, data, keep_mean): + return apply_mean_norm_trend( + pos=fld.pos, + field=data, + mean=None if keep_mean else fld.mean, + normalizer=fld.normalizer, + trend=fld.trend, + mesh_type=fld.mesh_type, + value_type=fld.value_type, + check_shape=False, + ) + + +def _check_for_default_normal(fld): + if type(fld.normalizer) is not Normalizer: + raise ValueError( + "transform: need a normal field but there is a normalizer defined" + ) + if fld.trend is not None: + raise ValueError( + "transform: need a normal field but there is a trend defined" + ) + if callable(fld.mean) or fld.mean is None: + raise ValueError( + "transform: need a normal field but mean is not constant" + ) + + +def apply(fld, method, field="field", store=True, process=False, **kwargs): + """ + Apply field transformation. + + Parameters + ---------- + fld : :any:`Field` + Field class containing a generated field. + method : :class:`str` + Method to use. + See :py:mod:`gstools.transform` for available transformations. + field : :class:`str`, optional + Name of field to be transformed. The default is "field". + store : :class:`str` or :class:`bool`, optional + Whether to store field inplace (True/False) or with a specified name. + The default is True. + process : :class:`bool`, optional + Whether to process in/out fields with trend, normalizer and mean + of given Field instance. The default is False. + **kwargs + Keyword arguments forwarded to selected method. + + Raises + ------ + ValueError + When method is unknown. + + Returns + ------- + :class:`numpy.ndarray` + Transformed field. + """ + kwargs["field"] = field + kwargs["store"] = store + kwargs["process"] = process + method = str(method) # ensure method is a string + if method == "binary": + return binary(fld, **kwargs) + if method == "discrete": + return discrete(fld, **kwargs) + if method == "boxcox": + return boxcox(fld, **kwargs) + if method == "zinnharvey": + return zinnharvey(fld, **kwargs) + if method.endswith("force_moments"): + return normal_force_moments(fld, **kwargs) + if method.endswith("lognormal"): + return normal_to_lognormal(fld, **kwargs) + if method.endswith("uniform"): + return normal_to_uniform(fld, **kwargs) + if method.endswith("arcsin"): + return normal_to_arcsin(fld, **kwargs) + if method.endswith("uquad"): + return normal_to_uquad(fld, **kwargs) + if method.endswith("function"): + return apply_function(fld, **kwargs) + raise ValueError(f"transform.apply: unknown method '{method}'") + + +def apply_function( + fld, + function, + field="field", + store=True, + process=False, + keep_mean=True, + **kwargs, +): + """ + Apply function as field transformation. + + Parameters + ---------- + fld : :any:`Field` + Field class containing a generated field. + function : :any:`callable` + Function to use. + field : :class:`str`, optional + Name of field to be transformed. The default is "field". + store : :class:`str` or :class:`bool`, optional + Whether to store field inplace (True/False) or under a given name. + The default is True. + process : :class:`bool`, optional + Whether to process in/out fields with trend, normalizer and mean + of given Field instance. The default is False. + keep_mean : :class:`bool`, optional + Whether to keep the mean of the field if process=True. + The default is True. + **kwargs + Keyword arguments forwarded to given function. + + Raises + ------ + ValueError + When function is not callable. + + Returns + ------- + :class:`numpy.ndarray` + Transformed field. + """ + if not callable(function): + raise ValueError("transform.apply_function: function not a 'callable'") + data = fld[field] + name, save = fld.get_store_config(store, default=field) + if process: + data = _pre_process(fld, data, keep_mean=keep_mean) + data = function(data, **kwargs) + if process: + data = _post_process(fld, data, keep_mean=keep_mean) + return fld.post_field(data, name=name, process=False, save=save) + + +def binary( + fld, + divide=None, + upper=None, + lower=None, + field="field", + store=True, + process=False, + keep_mean=True, +): + """ + Binary transformation. + + After this transformation, the field only has two values. + + Parameters + ---------- + fld : :any:`Field` + Field class containing a generated field. + divide : :class:`float`, optional + The dividing value. + Default: ``fld.mean`` + upper : :class:`float`, optional + The resulting upper value of the field. + Default: ``mean + sqrt(fld.model.sill)`` + lower : :class:`float`, optional + The resulting lower value of the field. + Default: ``mean - sqrt(fld.model.sill)`` + field : :class:`str`, optional + Name of field to be transformed. The default is "field". + store : :class:`str` or :class:`bool`, optional + Whether to store field inplace (True/False) or under a given name. + The default is True. + process : :class:`bool`, optional + Whether to process in/out fields with trend, normalizer and mean + of given Field instance. The default is False. + keep_mean : :class:`bool`, optional + Whether to keep the mean of the field if process=True. + The default is True. + + Returns + ------- + :class:`numpy.ndarray` + Transformed field. + """ + if not process and divide is None: + _check_for_default_normal(fld) + mean = 0.0 if process and not keep_mean else fld.mean + divide = mean if divide is None else divide + upper = mean + np.sqrt(fld.model.sill) if upper is None else upper + lower = mean - np.sqrt(fld.model.sill) if lower is None else lower + kw = dict( + values=[lower, upper], + thresholds=[divide], + ) + return apply_function( + fld=fld, + function=array_discrete, + field=field, + store=store, + process=process, + keep_mean=keep_mean, + **kw, + ) + + +def discrete( + fld, + values, + thresholds="arithmetic", + field="field", + store=True, + process=False, + keep_mean=True, +): + """ + Discrete transformation. + + After this transformation, the field has only `len(values)` discrete + values. + + Parameters + ---------- + fld : :any:`Field` + Field class containing a generated field. + values : :any:`numpy.ndarray` + The discrete values the field will take + thresholds : :class:`str` or :any:`numpy.ndarray`, optional + the thresholds, where the value classes are separated + possible values are: + * "arithmetic": the mean of the 2 neighbouring values + * "equal": divide the field into equal parts + * an array of explicitly given thresholds + Default: "arithmetic" + field : :class:`str`, optional + Name of field to be transformed. The default is "field". + store : :class:`str` or :class:`bool`, optional + Whether to store field inplace (True/False) or under a given name. + The default is True. + process : :class:`bool`, optional + Whether to process in/out fields with trend, normalizer and mean + of given Field instance. The default is False. + keep_mean : :class:`bool`, optional + Whether to keep the mean of the field if process=True. + The default is True. + + Returns + ------- + :class:`numpy.ndarray` + Transformed field. + """ + if not process and thresholds == "equal": + _check_for_default_normal(fld) + kw = dict( + values=values, + thresholds=thresholds, + mean=0.0 if process and not keep_mean else fld.mean, + var=fld.model.sill, + ) + return apply_function( + fld=fld, + function=array_discrete, + field=field, + store=store, + process=process, + keep_mean=keep_mean, + **kw, + ) + + +def boxcox( + fld, + lmbda=1, + shift=0, + field="field", + store=True, + process=False, + keep_mean=True, +): + """ + (Inverse) Box-Cox transformation to denormalize data. + + After this transformation, the again Box-Cox transformed field is normal + distributed. + + See: https://en.wikipedia.org/wiki/Power_transform#Box%E2%80%93Cox_transformation + + Parameters + ---------- + fld : :any:`Field` + Field class containing a generated field. + lmbda : :class:`float`, optional + The lambda parameter of the Box-Cox transformation. + For ``lmbda=0`` one obtains the log-normal transformation. + Default: ``1`` + shift : :class:`float`, optional + The shift parameter from the two-parametric Box-Cox transformation. + The field will be shifted by that value before transformation. + Default: ``0`` + field : :class:`str`, optional + Name of field to be transformed. The default is "field". + store : :class:`str` or :class:`bool`, optional + Whether to store field inplace (True/False) or under a given name. + The default is True. + process : :class:`bool`, optional + Whether to process in/out fields with trend, normalizer and mean + of given Field instance. The default is False. + keep_mean : :class:`bool`, optional + Whether to keep the mean of the field if process=True. + The default is True. + + Returns + ------- + :class:`numpy.ndarray` + Transformed field. + """ + kw = dict(lmbda=lmbda, shift=shift) + return apply_function( + fld=fld, + function=array_boxcox, + field=field, + store=store, + process=process, + keep_mean=keep_mean, + **kw, + ) + + +def zinnharvey( + fld, + conn="high", + field="field", + store=True, + process=False, + keep_mean=True, +): + """ + Zinn and Harvey transformation to connect low or high values. + + After this transformation, the field is still normal distributed. + + Parameters + ---------- + fld : :any:`Field` + Field class containing a generated field. + conn : :class:`str`, optional + Desired connectivity. Either "low" or "high". + Default: "high" + field : :class:`str`, optional + Name of field to be transformed. The default is "field". + store : :class:`str` or :class:`bool`, optional + Whether to store field inplace (True/False) or under a given name. + The default is True. + process : :class:`bool`, optional + Whether to process in/out fields with trend, normalizer and mean + of given Field instance. The default is False. + keep_mean : :class:`bool`, optional + Whether to keep the mean of the field if process=True. + The default is True. + + Returns + ------- + :class:`numpy.ndarray` + Transformed field. + """ + if not process: + _check_for_default_normal(fld) + kw = dict( + conn=conn, + mean=0.0 if process and not keep_mean else fld.mean, + var=fld.model.sill, + ) + return apply_function( + fld=fld, + function=array_zinnharvey, + field=field, + store=store, + process=process, + keep_mean=keep_mean, + **kw, + ) + + +def normal_force_moments( + fld, + field="field", + store=True, + process=False, + keep_mean=True, +): + """ + Force moments of a normal distributed field. + + After this transformation, the field is still normal distributed. + + Parameters + ---------- + fld : :any:`Field` + Field class containing a generated field. + field : :class:`str`, optional + Name of field to be transformed. The default is "field". + store : :class:`str` or :class:`bool`, optional + Whether to store field inplace (True/False) or under a given name. + The default is True. + process : :class:`bool`, optional + Whether to process in/out fields with trend, normalizer and mean + of given Field instance. The default is False. + keep_mean : :class:`bool`, optional + Whether to keep the mean of the field if process=True. + The default is True. + + Returns + ------- + :class:`numpy.ndarray` + Transformed field. + """ + if not process: + _check_for_default_normal(fld) + kw = dict( + mean=0.0 if process and not keep_mean else fld.mean, var=fld.model.sill + ) + return apply_function( + fld=fld, + function=array_force_moments, + field=field, + store=store, + process=process, + keep_mean=keep_mean, + **kw, + ) + + +def normal_to_lognormal( + fld, field="field", store=True, process=False, keep_mean=True +): + """ + Transform normal distribution to log-normal distribution. + + After this transformation, the field is log-normal distributed. + + Parameters + ---------- + fld : :any:`Field` + Field class containing a generated field. + field : :class:`str`, optional + Name of field to be transformed. The default is "field". + store : :class:`str` or :class:`bool`, optional + Whether to store field inplace (True/False) or under a given name. + The default is True. + process : :class:`bool`, optional + Whether to process in/out fields with trend, normalizer and mean + of given Field instance. The default is False. + keep_mean : :class:`bool`, optional + Whether to keep the mean of the field if process=True. + The default is True. + + Returns + ------- + :class:`numpy.ndarray` + Transformed field. + """ + return apply_function( + fld=fld, + function=array_to_lognormal, + field=field, + store=store, + process=process, + keep_mean=keep_mean, + ) + + +def normal_to_uniform( + fld, + low=0.0, + high=1.0, + field="field", + store=True, + process=False, + keep_mean=True, +): + """ + Transform normal distribution to uniform distribution on [0, 1]. + + After this transformation, the field is uniformly distributed on [0, 1]. + + Parameters + ---------- + fld : :any:`Field` + Field class containing a generated field. + low : :class:`float`, optional + Lower bound for the uniform distribution. + Default: 0.0 + high : :class:`float`, optional + Upper bound for the uniform distribution. + Default: 1.0 + field : :class:`str`, optional + Name of field to be transformed. The default is "field". + store : :class:`str` or :class:`bool`, optional + Whether to store field inplace (True/False) or under a given name. + The default is True. + process : :class:`bool`, optional + Whether to process in/out fields with trend, normalizer and mean + of given Field instance. The default is False. + keep_mean : :class:`bool`, optional + Whether to keep the mean of the field if process=True. + The default is True. + """ + if not process: + _check_for_default_normal(fld) + kw = dict( + mean=0.0 if process and not keep_mean else fld.mean, + var=fld.model.sill, + low=low, + high=high, + ) + return apply_function( + fld=fld, + function=array_to_uniform, + field=field, + store=store, + process=process, + keep_mean=keep_mean, + **kw, + ) + + +def normal_to_arcsin( + fld, + a=None, + b=None, + field="field", + store=True, + process=False, + keep_mean=True, +): + """ + Transform normal distribution to the bimodal arcsin distribution. + + See: https://en.wikipedia.org/wiki/Arcsine_distribution + + After this transformation, the field is arcsin-distributed on [a, b]. + + Parameters + ---------- + fld : :any:`Field` + Field class containing a generated field. + a : :class:`float`, optional + Parameter a of the arcsin distribution (lower bound). + Default: keep mean and variance + b : :class:`float`, optional + Parameter b of the arcsin distribution (upper bound). + Default: keep mean and variance + field : :class:`str`, optional + Name of field to be transformed. The default is "field". + store : :class:`str` or :class:`bool`, optional + Whether to store field inplace (True/False) or under a given name. + The default is True. + process : :class:`bool`, optional + Whether to process in/out fields with trend, normalizer and mean + of given Field instance. The default is False. + keep_mean : :class:`bool`, optional + Whether to keep the mean of the field if process=True. + The default is True. + + Returns + ------- + :class:`numpy.ndarray` + Transformed field. + """ + if not process: + _check_for_default_normal(fld) + kw = dict( + mean=0.0 if process and not keep_mean else fld.mean, + var=fld.model.sill, + a=a, + b=b, + ) + return apply_function( + fld=fld, + function=array_to_arcsin, + field=field, + store=store, + process=process, + keep_mean=keep_mean, + **kw, + ) + + +def normal_to_uquad( + fld, + a=None, + b=None, + field="field", + store=True, + process=False, + keep_mean=True, +): + """ + Transform normal distribution to U-quadratic distribution. + + See: https://en.wikipedia.org/wiki/U-quadratic_distribution + + After this transformation, the field is U-quadratic-distributed on [a, b]. + + Parameters + ---------- + fld : :any:`Field` + Field class containing a generated field. + a : :class:`float`, optional + Parameter a of the U-quadratic distribution (lower bound). + Default: keep mean and variance + b : :class:`float`, optional + Parameter b of the U-quadratic distribution (upper bound). + Default: keep mean and variance + field : :class:`str`, optional + Name of field to be transformed. The default is "field". + store : :class:`str` or :class:`bool`, optional + Whether to store field inplace (True/False) or under a given name. + The default is True. + process : :class:`bool`, optional + Whether to process in/out fields with trend, normalizer and mean + of given Field instance. The default is False. + keep_mean : :class:`bool`, optional + Whether to keep the mean of the field if process=True. + The default is True. + + Returns + ------- + :class:`numpy.ndarray` + Transformed field. + """ + if not process: + _check_for_default_normal(fld) + kw = dict( + mean=0.0 if process and not keep_mean else fld.mean, + var=fld.model.sill, + a=a, + b=b, + ) + return apply_function( + fld=fld, + function=array_to_uquad, + field=field, + store=store, + process=process, + keep_mean=keep_mean, + **kw, + ) diff --git a/GSTools/source/src/gstools/variogram/__init__.py b/GSTools/source/src/gstools/variogram/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d8a5b238d73c986a346954dc635fa8b849f0495a --- /dev/null +++ b/GSTools/source/src/gstools/variogram/__init__.py @@ -0,0 +1,40 @@ +""" +GStools subpackage providing tools for estimating and fitting variograms. + +.. currentmodule:: gstools.variogram + +Variogram estimation +^^^^^^^^^^^^^^^^^^^^ + +.. autosummary:: + :toctree: + + vario_estimate + vario_estimate_axis + +Binning +^^^^^^^ + +.. autosummary:: + :toctree: + + standard_bins + +---- +""" + +from gstools.variogram.binning import standard_bins +from gstools.variogram.variogram import ( + vario_estimate, + vario_estimate_axis, + vario_estimate_structured, + vario_estimate_unstructured, +) + +__all__ = [ + "vario_estimate", + "vario_estimate_axis", + "vario_estimate_unstructured", + "vario_estimate_structured", + "standard_bins", +] diff --git a/GSTools/source/src/gstools/variogram/binning.py b/GSTools/source/src/gstools/variogram/binning.py new file mode 100644 index 0000000000000000000000000000000000000000..86d4fdc2727624a83f08b38f9d6bafc98ed950bd --- /dev/null +++ b/GSTools/source/src/gstools/variogram/binning.py @@ -0,0 +1,104 @@ +""" +GStools subpackage providing binning routines. + +.. currentmodule:: gstools.variogram.binning + +The following functions are provided + +.. autosummary:: + standard_bins +""" + +import numpy as np + +from gstools.tools import RADIAN_SCALE +from gstools.tools.geometric import ( + chordal_to_great_circle, + format_struct_pos_dim, + generate_grid, + latlon2pos, +) + +__all__ = ["standard_bins"] + + +def _sturges(pnt_cnt): + return int(np.ceil(2 * np.log2(pnt_cnt) + 1)) + + +def standard_bins( + pos=None, + dim=2, + latlon=False, + mesh_type="unstructured", + bin_no=None, + max_dist=None, + geo_scale=RADIAN_SCALE, +): + r""" + Get standard binning. + + Parameters + ---------- + pos : :class:`list`, optional + the position tuple, containing either the point coordinates (x, y, ...) + or the axes descriptions (for mesh_type='structured') + dim : :class:`int`, optional + Field dimension. + latlon : :class:`bool`, optional + Whether the data is representing 2D fields on earths surface described + by latitude and longitude. When using this, the estimator will + use great-circle distance for variogram estimation. + Note, that only an isotropic variogram can be estimated and a + ValueError will be raised, if a direction was specified. + Bin edges need to be given in radians in this case. + Default: False + mesh_type : :class:`str`, optional + 'structured' / 'unstructured', indicates whether the pos tuple + describes the axis or the point coordinates. + Default: `'unstructured'` + bin_no: :class:`int`, optional + number of bins to create. If None is given, will be determined by + Sturges' rule from the number of points. + Default: None + max_dist: :class:`float`, optional + Cut of length for the bins. If None is given, it will be set to one + third of the box-diameter from the given points. + Default: None + geo_scale : :class:`float`, optional + Geographic unit scaling in case of latlon coordinates to get a + meaningful bins unit. + By default, bins are assumed to be in radians with latlon=True. + Can be set to :any:`KM_SCALE` to have bins in km or + :any:`DEGREE_SCALE` to have bins in degrees. + Default: :any:`RADIAN_SCALE` + + Returns + ------- + :class:`numpy.ndarray` + The generated bin edges. + + Notes + ----- + Internally uses double precision and also returns doubles. + """ + dim = 2 if latlon else int(dim) + if bin_no is None or max_dist is None: + if pos is None: + raise ValueError("standard_bins: no pos tuple given.") + if mesh_type != "unstructured": + pos = generate_grid(format_struct_pos_dim(pos, dim)[0]) + else: + pos = np.asarray(pos, dtype=np.double).reshape(dim, -1) + pos = latlon2pos(pos, radius=geo_scale) if latlon else pos + pnt_cnt = len(pos[0]) + box = [] + for axis in pos: + box.append([np.min(axis), np.max(axis)]) + box = np.asarray(box) + diam = np.linalg.norm(box[:, 0] - box[:, 1]) + # convert diameter to great-circle distance if using latlon + diam = chordal_to_great_circle(diam, geo_scale) if latlon else diam + bin_no = _sturges(pnt_cnt) if bin_no is None else int(bin_no) + max_dist = diam / 3 if max_dist is None else float(max_dist) + return np.linspace(0, max_dist, num=bin_no + 1, dtype=np.double) diff --git a/GSTools/source/src/gstools/variogram/variogram.py b/GSTools/source/src/gstools/variogram/variogram.py new file mode 100644 index 0000000000000000000000000000000000000000..4dc9983a83d5965f0a90e2490c05f269038e914e --- /dev/null +++ b/GSTools/source/src/gstools/variogram/variogram.py @@ -0,0 +1,572 @@ +""" +GStools subpackage providing tools for estimating and fitting variograms. + +.. currentmodule:: gstools.variogram.variogram + +The following functions are provided + +.. autosummary:: + vario_estimate + vario_estimate_axis +""" + +import numpy as np +from gstools_cython.variogram import directional as directional_c +from gstools_cython.variogram import ma_structured as ma_structured_c +from gstools_cython.variogram import structured as structured_c +from gstools_cython.variogram import unstructured as unstructured_c + +from gstools import config +from gstools.normalizer.tools import remove_trend_norm_mean +from gstools.tools import RADIAN_SCALE +from gstools.tools.geometric import ( + ang2dir, + format_struct_pos_shape, + format_unstruct_pos_shape, + generate_grid, +) +from gstools.variogram.binning import standard_bins + +if config._GSTOOLS_CORE_AVAIL: # pragma: no cover + from gstools_core import variogram_directional as directional_gsc + from gstools_core import variogram_ma_structured as ma_structured_gsc + from gstools_core import variogram_structured as structured_gsc + from gstools_core import variogram_unstructured as unstructured_gsc + +__all__ = [ + "vario_estimate", + "vario_estimate_axis", + "vario_estimate_unstructured", + "vario_estimate_structured", +] + + +AXIS = ["x", "y", "z"] +AXIS_DIR = {"x": 0, "y": 1, "z": 2} + + +def _directional( + field, + bin_edges, + pos, + direction, + angles_tol=np.pi / 8.0, + bandwidth=-1.0, + separate_dirs=False, + estimator_type="m", + num_threads=None, +): + """A wrapper function for calling the directional variogram algorithms.""" + if config.USE_GSTOOLS_CORE and config._GSTOOLS_CORE_AVAIL: + directional_fct = directional_gsc + else: + directional_fct = directional_c + return directional_fct( + field, + bin_edges, + pos, + direction, + angles_tol, + bandwidth, + separate_dirs, + estimator_type, + num_threads, + ) + + +def _unstructured( + field, + bin_edges, + pos, + estimator_type="m", + distance_type="e", + num_threads=None, +): + """A wrapper function for calling the unstructured variogram algorithms.""" + if config.USE_GSTOOLS_CORE and config._GSTOOLS_CORE_AVAIL: + unstructured_fct = unstructured_gsc + else: + unstructured_fct = unstructured_c + return unstructured_fct( + field, + bin_edges, + pos, + estimator_type, + distance_type, + num_threads, + ) + + +def _structured( + field, + estimator_type="m", + num_threads=None, +): + """A wrapper function for calling the structured variogram algorithms.""" + if config.USE_GSTOOLS_CORE and config._GSTOOLS_CORE_AVAIL: + structured_fct = structured_gsc + else: + structured_fct = structured_c + return structured_fct(field, estimator_type, num_threads) + + +def _ma_structured( + field, + mask, + estimator_type="m", + num_threads=None, +): + """A wrapper function for calling the masked struct. variogram algorithms.""" + if config.USE_GSTOOLS_CORE and config._GSTOOLS_CORE_AVAIL: + ma_structured_fct = ma_structured_gsc + else: + ma_structured_fct = ma_structured_c + return ma_structured_fct(field, mask, estimator_type, num_threads) + + +def _set_estimator(estimator): + """Translate the verbose Python estimator identifier to single char.""" + if estimator.lower() == "matheron": + cython_estimator = "m" + elif estimator.lower() == "cressie": + cython_estimator = "c" + else: + raise ValueError(f"Unknown variogram estimator function: {estimator}") + return cython_estimator + + +def _separate_dirs_test(direction, angles_tol): + """Check if given directions are separated.""" + if direction is None or direction.shape[0] < 2: + return True + separate_dirs = True + for i in range(direction.shape[0] - 1): + for j in range(i + 1, direction.shape[0]): + s_prod = np.minimum(np.abs(np.dot(direction[i], direction[j])), 1) + separate_dirs &= np.arccos(s_prod) >= 2 * angles_tol + # gstools-core doesn't like the type `numpy.bool_` + return bool(separate_dirs) + + +def vario_estimate( + pos, + field, + bin_edges=None, + sampling_size=None, + sampling_seed=None, + estimator="matheron", + latlon=False, + direction=None, + angles=None, + angles_tol=np.pi / 8, + bandwidth=None, + no_data=np.nan, + mask=np.ma.nomask, + mesh_type="unstructured", + return_counts=False, + mean=None, + normalizer=None, + trend=None, + fit_normalizer=False, + geo_scale=RADIAN_SCALE, + **std_bins, +): + r""" + Estimates the empirical variogram. + + The algorithm calculates following equation: + + .. math:: + \gamma(r_k) = \frac{1}{2 N(r_k)} \sum_{i=1}^{N(r_k)} (z(\mathbf x_i) - + z(\mathbf x_i'))^2 \; , + + with :math:`r_k \leq \| \mathbf x_i - \mathbf x_i' \| < r_{k+1}` + being the bins. + + Or if the estimator "cressie" was chosen: + + .. math:: + \gamma(r_k) = \frac{\frac{1}{2}\left(\frac{1}{N(r_k)}\sum_{i=1}^{N(r_k)} + \left|z(\mathbf x_i) - z(\mathbf x_i')\right|^{0.5}\right)^4} + {0.457 + 0.494 / N(r_k) + 0.045 / N^2(r_k)} \; , + + with :math:`r_k \leq \| \mathbf x_i - \mathbf x_i' \| < r_{k+1}` + being the bins. + The Cressie estimator is more robust to outliers [Webster2007]_. + + By providing `direction` vector[s] or angles, a directional variogram + can be calculated. If multiple directions are given, a set of variograms + will be returned. + Directional bining is controlled by a given angle tolerance (`angles_tol`) + and an optional `bandwidth`, that truncates the width of the search band + around the given direction[s]. + + To reduce the calculation time, `sampling_size` could be passed to sample + down the number of field points. + + Parameters + ---------- + pos : :class:`list` + the position tuple, containing either the point coordinates (x, y, ...) + or the axes descriptions (for mesh_type='structured') + field : :class:`numpy.ndarray` or :class:`list` of :class:`numpy.ndarray` + The spatially distributed data. + Can also be of type :class:`numpy.ma.MaskedArray` to use masked values. + You can pass a list of fields, that will be used simultaneously. + This could be helpful, when there are multiple realizations at the + same points, with the same statistical properties. + bin_edges : :class:`numpy.ndarray`, optional + the bins on which the variogram will be calculated. + If :any:`None` are given, standard bins provided by the + :any:`standard_bins` routine will be used. Default: :any:`None` + sampling_size : :class:`int` or :any:`None`, optional + for large input data, this method can take a long + time to compute the variogram, therefore this argument specifies + the number of data points to sample randomly + Default: :any:`None` + sampling_seed : :class:`int` or :any:`None`, optional + seed for samples if sampling_size is given. + Default: :any:`None` + estimator : :class:`str`, optional + the estimator function, possible choices: + + * "matheron": the standard method of moments of Matheron + * "cressie": an estimator more robust to outliers + + Default: "matheron" + latlon : :class:`bool`, optional + Whether the data is representing 2D fields on earths surface described + by latitude and longitude. When using this, the estimator will + use great-circle distance for variogram estimation. + Note, that only an isotropic variogram can be estimated and a + ValueError will be raised, if a direction was specified. + Bin edges need to be given in radians in this case. + Default: False + direction : :class:`list` of :class:`numpy.ndarray`, optional + directions to evaluate a directional variogram. + Angular tolerance is given by `angles_tol`. + bandwidth to cut off how wide the search for point pairs should be + is given by `bandwidth`. + You can provide multiple directions at once to get one variogram + for each direction. + For a single direction you can also use the `angles` parameter, + to provide the direction by its spherical coordinates. + Default: :any:`None` + angles : :class:`numpy.ndarray`, optional + the angles of the main axis to calculate the variogram for in radians + angle definitions from ISO standard 80000-2:2009 + for 1d this parameter will have no effect at all + for 2d supply one angle which is + azimuth :math:`\varphi` (ccw from +x in xy plane) + for 3d supply two angles which are + azimuth :math:`\varphi` (ccw from +x in xy plane) + and inclination :math:`\theta` (cw from +z). + Can be used instead of direction. + Default: :any:`None` + angles_tol : class:`float`, optional + the tolerance around the variogram angle to count a point as being + within this direction from another point (the angular tolerance around + the directional vector given by angles) + Default: `np.pi/8` = 22.5° + bandwidth : class:`float`, optional + bandwidth to cut off the angular tolerance for directional variograms. + If None is given, only the `angles_tol` parameter will control the + point selection. + Default: :any:`None` + no_data : :class:`float`, optional + Value to identify missing data in the given field. + Default: `numpy.nan` + mask : :class:`numpy.ndarray` of :class:`bool`, optional + Mask to deselect data in the given field. + Default: :any:`numpy.ma.nomask` + mesh_type : :class:`str`, optional + 'structured' / 'unstructured', indicates whether the pos tuple + describes the axis or the point coordinates. + Default: `'unstructured'` + return_counts: :class:`bool`, optional + if set to true, this function will also return the number of data + points found at each lag distance as a third return value + Default: False + mean : :class:`float`, optional + mean value used to shift normalized input data. + Can also be a callable. The default is None. + normalizer : :any:`None` or :any:`Normalizer`, optional + Normalizer to be applied to the input data to gain normality. + The default is None. + trend : :any:`None` or :class:`float` or :any:`callable`, optional + A callable trend function. Should have the signature: f(x, [y, z, ...]) + If no normalizer is applied, this behaves equal to 'mean'. + The default is None. + fit_normalizer : :class:`bool`, optional + Whether to fit the data-normalizer to the given (detrended) field. + Default: False + geo_scale : :class:`float`, optional + Geographic unit scaling in case of latlon coordinates to get a + meaningful bins unit. + By default, bins are assumed to be in radians with latlon=True. + Can be set to :any:`KM_SCALE` to have bins in km or + :any:`DEGREE_SCALE` to have bins in degrees. + Default: :any:`RADIAN_SCALE` + **std_bins + Optional arguments that are forwarded to the :any:`standard_bins` routine + if no bins are given (bin_no, max_dist). + + Returns + ------- + bin_centers : (n), :class:`numpy.ndarray` + The bin centers. + gamma : (n) or (d, n), :class:`numpy.ndarray` + The estimated variogram values at bin centers. + Is stacked if multiple `directions` (d>1) are given. + counts : (n) or (d, n), :class:`numpy.ndarray`, optional + The number of point pairs found for each bin. + Is stacked if multiple `directions` (d>1) are given. + Only provided if `return_counts` is True. + normalizer : :any:`Normalizer`, optional + The fitted normalizer for the given data. + Only provided if `fit_normalizer` is True. + + Notes + ----- + Internally uses double precision and also returns doubles. + + References + ---------- + .. [Webster2007] Webster, R. and Oliver, M. A. + "Geostatistics for environmental scientists.", + John Wiley & Sons. (2007) + """ + if bin_edges is not None: + bin_edges = np.atleast_1d(np.asarray(bin_edges, dtype=np.double)) + bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2.0 + # allow multiple fields at same positions (ndmin=2: first axis -> field ID) + # need to convert to ma.array, since list of ma.array is not recognised + field = np.ma.array(field, ndmin=2, dtype=np.double, copy=True) + masked = np.ma.is_masked(field) or np.any(mask) + # catch special case if everything is masked + if masked and np.all(mask): + bin_centers = np.empty(0) if bin_edges is None else bin_centers + estimates = np.zeros_like(bin_centers) + if return_counts: + return bin_centers, estimates, np.zeros_like(estimates, dtype=int) + return bin_centers, estimates + if not masked: + field = field.filled() + # check mesh shape + if mesh_type != "unstructured": + pos, __, dim = format_struct_pos_shape( + pos, field.shape, check_stacked_shape=True + ) + pos = generate_grid(pos) + else: + pos, __, dim = format_unstruct_pos_shape( + pos, field.shape, check_stacked_shape=True + ) + if latlon and dim != 2: + raise ValueError("Variogram: given field needs to be 2D for lat-lon.") + # prepare the field + pnt_cnt = len(pos[0]) + field = field.reshape((-1, pnt_cnt)) + # apply mask if wanted + if masked: + # if fields have different masks, take the minimal common mask + # given mask will be applied in addition + # selected region is the inverted masked (unmasked values) + if np.size(mask) > 1: # not only np.ma.nomask + select = np.invert( + np.logical_or( + np.reshape(mask, pnt_cnt), np.all(field.mask, axis=0) + ) + ) + else: + select = np.invert(np.all(field.mask, axis=0)) + pos = pos[:, select] + field.fill_value = np.nan # use no-data val. for remaining masked vals + field = field[:, select].filled() # convert to ndarray + select = mask = None # free space + pnt_cnt = len(pos[0]) # pnt cnt reduced now + # set no_data values + if not np.isnan(no_data): + field[np.isclose(field, float(no_data))] = np.nan + # set directions + dir_no = 0 + if direction is not None and dim > 1: + direction = np.atleast_2d(np.asarray(direction, dtype=np.double)) + if len(direction.shape) > 2: + raise ValueError(f"Can't interpret directions: {direction}") + if direction.shape[1] != dim: + raise ValueError(f"Can't interpret directions: {direction}") + dir_no = direction.shape[0] + # convert given angles to direction vector + if angles is not None and direction is None and dim > 1: + direction = ang2dir(angles=angles, dtype=np.double, dim=dim) + dir_no = direction.shape[0] + # prepare directional variogram + if dir_no > 0: + if latlon: + raise ValueError("Directional variogram not allowed for lat-lon.") + norms = np.linalg.norm(direction, axis=1) + if np.any(np.isclose(norms, 0)): + raise ValueError(f"Zero length directions: {direction}") + # only unit-vectors for directions + direction = np.divide(direction, norms[:, np.newaxis]) + # negative bandwidth to turn it off + bandwidth = float(bandwidth) if bandwidth is not None else -1.0 + angles_tol = float(angles_tol) + # prepare sampled variogram + if sampling_size is not None and sampling_size < pnt_cnt: + sampled_idx = np.random.RandomState(sampling_seed).choice( + np.arange(pnt_cnt), sampling_size, replace=False + ) + field = field[:, sampled_idx] + pos = pos[:, sampled_idx] + # create bins + if bin_edges is None: + bin_edges = standard_bins( + pos, dim, latlon, geo_scale=geo_scale, **std_bins + ) + bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2.0 + if latlon: + # internally we always use radians + bin_edges /= geo_scale + # normalize field + norm_field_out = remove_trend_norm_mean( + *(pos, field, mean, normalizer, trend), + check_shape=False, + stacked=True, + fit_normalizer=fit_normalizer, + ) + field = norm_field_out[0] if fit_normalizer else norm_field_out + norm_out = (norm_field_out[1],) if fit_normalizer else () + # select variogram estimator + cython_estimator = _set_estimator(estimator) + # run + if dir_no == 0: + # "h"aversine or "e"uclidean distance type + distance_type = "h" if latlon else "e" + estimates, counts = _unstructured( + field, + bin_edges, + pos, + estimator_type=cython_estimator, + distance_type=distance_type, + num_threads=config.NUM_THREADS, + ) + else: + estimates, counts = _directional( + field, + bin_edges, + pos, + direction, + angles_tol, + bandwidth, + separate_dirs=_separate_dirs_test(direction, angles_tol), + estimator_type=cython_estimator, + num_threads=config.NUM_THREADS, + ) + if dir_no == 1: + estimates, counts = estimates[0], counts[0] + est_out = (estimates, counts) + return (bin_centers,) + est_out[: 2 if return_counts else 1] + norm_out + + +def vario_estimate_axis( + field, direction="x", estimator="matheron", no_data=np.nan +): + r"""Estimates the variogram along array axis. + + The indices of the given direction are used for the bins. + Uniform spacings along the given axis are assumed. + + The algorithm calculates following equation: + + .. math:: + \gamma(r_k) = \frac{1}{2 N(r_k)} \sum_{i=1}^{N(r_k)} (z(\mathbf x_i) - + z(\mathbf x_i'))^2 \; , + + with :math:`r_k \leq \| \mathbf x_i - \mathbf x_i' \| < r_{k+1}` + being the bins. + + Or if the estimator "cressie" was chosen: + + .. math:: + \gamma(r_k) = \frac{\frac{1}{2}\left(\frac{1}{N(r_k)}\sum_{i=1}^{N(r_k)} + \left|z(\mathbf x_i) - z(\mathbf x_i')\right|^{0.5}\right)^4} + {0.457 + 0.494 / N(r_k) + 0.045 / N^2(r_k)} \; , + + with :math:`r_k \leq \| \mathbf x_i - \mathbf x_i' \| < r_{k+1}` + being the bins. + The Cressie estimator is more robust to outliers [Webster2007]_. + + Parameters + ---------- + field : :class:`numpy.ndarray` or :class:`numpy.ma.MaskedArray` + the spatially distributed data (can be masked) + direction : :class:`str` or :class:`int` + the axis over which the variogram will be estimated (x, y, z) + or (0, 1, 2, ...) + estimator : :class:`str`, optional + the estimator function, possible choices: + + * "matheron": the standard method of moments of Matheron + * "cressie": an estimator more robust to outliers + + Default: "matheron" + + no_data : :class:`float`, optional + Value to identify missing data in the given field. + Default: `numpy.nan` + + Returns + ------- + :class:`numpy.ndarray` + the estimated variogram along the given direction. + + Warnings + -------- + It is assumed that the field is defined on an equidistant Cartesian grid. + + Notes + ----- + Internally uses double precision and also returns doubles. + + References + ---------- + .. [Webster2007] Webster, R. and Oliver, M. A. + "Geostatistics for environmental scientists.", + John Wiley & Sons. (2007) + """ + missing_mask = ( + np.isnan(field) if np.isnan(no_data) else np.isclose(field, no_data) + ) + missing = np.any(missing_mask) + masked = np.ma.is_masked(field) or missing + if masked: + field = np.ma.array(field, ndmin=1, dtype=np.double) + if missing: + field.mask = np.logical_or(field.mask, missing_mask) + mask = np.ma.getmaskarray(field) + else: + field = np.atleast_1d(np.asarray(field, dtype=np.double)) + missing_mask = None # free space + + axis_to_swap = AXIS_DIR[direction] if direction in AXIS else int(direction) + # desired axis first, convert to 2D array afterwards + field = field.swapaxes(0, axis_to_swap) + field = field.reshape((field.shape[0], -1)) + if masked: + mask = mask.swapaxes(0, axis_to_swap) + mask = mask.reshape((mask.shape[0], -1)) + + cython_estimator = _set_estimator(estimator) + + if masked: + return _ma_structured( + field, mask, cython_estimator, num_threads=config.NUM_THREADS + ) + return _structured(field, cython_estimator, num_threads=config.NUM_THREADS) + + +# for backward compatibility +vario_estimate_unstructured = vario_estimate +vario_estimate_structured = vario_estimate_axis diff --git a/GSTools/source/tests/data/variogram.txt b/GSTools/source/tests/data/variogram.txt new file mode 100644 index 0000000000000000000000000000000000000000..cad2a2be359fd9d24caf22fa5bef0570ccf86326 --- /dev/null +++ b/GSTools/source/tests/data/variogram.txt @@ -0,0 +1,2 @@ +2.500000000000000000e-01 7.500000000000000000e-01 1.250000000000000000e+00 1.750000000000000000e+00 2.250000000000000000e+00 2.750000000000000000e+00 3.250000000000000000e+00 3.750000000000000000e+00 4.250000000000000000e+00 4.750000000000000000e+00 5.250000000000000000e+00 5.750000000000000000e+00 6.250000000000000000e+00 6.750000000000000000e+00 7.250000000000000000e+00 7.750000000000000000e+00 8.250000000000000000e+00 8.750000000000000000e+00 9.250000000000000000e+00 9.750000000000000000e+00 1.025000000000000000e+01 1.075000000000000000e+01 1.125000000000000000e+01 1.175000000000000000e+01 1.225000000000000000e+01 1.275000000000000000e+01 1.325000000000000000e+01 1.375000000000000000e+01 1.425000000000000000e+01 1.475000000000000000e+01 1.525000000000000000e+01 1.575000000000000000e+01 1.625000000000000000e+01 1.675000000000000000e+01 1.725000000000000000e+01 1.775000000000000000e+01 1.825000000000000000e+01 1.875000000000000000e+01 1.925000000000000000e+01 1.975000000000000000e+01 2.025000000000000000e+01 2.075000000000000000e+01 2.125000000000000000e+01 2.175000000000000000e+01 2.225000000000000000e+01 2.275000000000000000e+01 2.325000000000000000e+01 2.375000000000000000e+01 2.425000000000000000e+01 2.475000000000000000e+01 2.525000000000000000e+01 2.575000000000000000e+01 2.625000000000000000e+01 2.675000000000000000e+01 2.725000000000000000e+01 2.775000000000000000e+01 2.825000000000000000e+01 2.875000000000000000e+01 2.925000000000000000e+01 2.975000000000000000e+01 3.025000000000000000e+01 3.075000000000000000e+01 3.125000000000000000e+01 3.175000000000000000e+01 3.225000000000000000e+01 3.275000000000000000e+01 3.325000000000000000e+01 3.375000000000000000e+01 3.425000000000000000e+01 3.475000000000000000e+01 3.525000000000000000e+01 3.575000000000000000e+01 3.625000000000000000e+01 3.675000000000000000e+01 3.725000000000000000e+01 3.775000000000000000e+01 3.825000000000000000e+01 3.875000000000000000e+01 3.925000000000000000e+01 3.975000000000000000e+01 4.025000000000000000e+01 4.075000000000000000e+01 4.125000000000000000e+01 4.175000000000000000e+01 4.225000000000000000e+01 4.275000000000000000e+01 4.325000000000000000e+01 4.375000000000000000e+01 4.425000000000000000e+01 4.475000000000000000e+01 4.525000000000000000e+01 4.575000000000000000e+01 4.625000000000000000e+01 4.675000000000000000e+01 4.725000000000000000e+01 4.775000000000000000e+01 4.825000000000000000e+01 4.875000000000000000e+01 4.925000000000000000e+01 4.975000000000000000e+01 +9.341287109808403821e-01 8.453134048372161757e-01 1.097876163640169045e+00 1.452675652987244215e+00 1.717011001508209400e+00 1.982144808494284316e+00 2.116570368080190168e+00 2.354956050093370834e+00 2.256700499821684858e+00 2.608287292075601727e+00 3.019563428869475263e+00 3.005582884425558987e+00 3.026403707124349474e+00 2.920331468705290145e+00 2.739513460596619598e+00 2.983946141180413569e+00 3.002935993020715788e+00 3.098828622420069401e+00 3.252632934536162423e+00 2.972322302660843629e+00 3.162730649439238206e+00 3.104794978590492249e+00 3.129840716346046658e+00 3.374897140494525605e+00 3.281178715947507207e+00 3.414929159332046993e+00 3.366128007332230609e+00 3.350479282803380254e+00 3.274833816819866961e+00 3.146498371924009607e+00 3.098730655077019946e+00 3.203013412974681895e+00 3.386168318931909393e+00 3.349129048195039093e+00 3.403803954611331228e+00 3.228624506901822677e+00 3.320464283280209816e+00 3.312375916258147424e+00 3.476587847562206068e+00 3.293083753810556846e+00 3.526290649886577366e+00 3.576052045142498859e+00 3.440057124681327405e+00 3.388464225293542853e+00 3.350244356710671667e+00 3.502962399475493704e+00 3.519679087894428626e+00 3.563334300898933993e+00 3.540069270604898843e+00 3.743566023712460389e+00 3.586432982353473964e+00 3.820556981535015773e+00 3.821630249625350473e+00 3.674450536968612013e+00 3.935179864707886388e+00 3.829663738092724312e+00 3.767186362291683466e+00 3.829516094024750128e+00 3.939988520639752245e+00 3.788358175483068191e+00 3.929699245388040385e+00 3.794618804853678196e+00 3.732527204313444535e+00 3.691846735135440483e+00 3.504551184978119682e+00 3.866643330145578261e+00 3.768275820398073073e+00 3.645363755293978603e+00 3.835573383488326105e+00 3.811532952179032208e+00 3.609181746819485781e+00 3.652959035004343491e+00 3.766941214399454285e+00 3.884337832265876589e+00 3.818059727339796705e+00 3.957999650541710324e+00 3.862889397483654896e+00 3.649107551259477056e+00 3.703662969618971346e+00 3.775887102684075014e+00 3.666801729040440438e+00 3.798614706003481167e+00 3.592569765506981483e+00 3.617445665128535826e+00 3.746142254086667567e+00 3.634920395024769935e+00 3.685908603892140256e+00 3.552532538928491412e+00 3.704709972576425869e+00 3.510318544036457933e+00 3.557027380970486874e+00 3.431417669026475270e+00 3.625033767218668324e+00 3.646369536691860258e+00 3.645946180745845311e+00 3.629191872454593071e+00 3.668750066947414457e+00 3.626446691977532222e+00 3.651811593215607665e+00 3.623437445821843017e+00 diff --git a/GSTools/source/tests/test_condition.py b/GSTools/source/tests/test_condition.py new file mode 100644 index 0000000000000000000000000000000000000000..8d5d0535e19ff56efd3c3c65c5cf2b42aea110c5 --- /dev/null +++ b/GSTools/source/tests/test_condition.py @@ -0,0 +1,159 @@ +"""This is the unittest of CondSRF class.""" + +import unittest +from copy import copy + +import numpy as np + +import gstools as gs + + +class TestCondition(unittest.TestCase): + def setUp(self): + self.cov_models = [ + gs.Gaussian, + gs.Exponential, + ] + self.dims = range(1, 4) + self.data = np.array( + [ + [0.3, 1.2, 0.5, 0.47], + [1.9, 0.6, 1.0, 0.56], + [1.1, 3.2, 1.5, 0.74], + [3.3, 4.4, 2.0, 1.47], + [4.7, 3.8, 2.5, 1.74], + ] + ) + self.cond_pos = (self.data[:, 0], self.data[:, 1], self.data[:, 2]) + self.cond_val = self.data[:, 3] + self.mean = np.mean(self.cond_val) + grid = np.linspace(5, 20, 10) + self.grid_x = np.concatenate((self.cond_pos[0], grid)) + self.grid_y = np.concatenate((self.cond_pos[1], grid)) + self.grid_z = np.concatenate((self.cond_pos[2], grid)) + self.pos = (self.grid_x, self.grid_y, self.grid_z) + + def test_simple(self): + for Model in self.cov_models: + model = Model( + dim=1, var=0.5, len_scale=2, anis=[0.1, 1], angles=[0.5, 0, 0] + ) + krige = gs.krige.Simple( + model, self.cond_pos[0], self.cond_val, self.mean + ) + crf = gs.CondSRF(krige, seed=19970221) + field_1 = crf.unstructured(self.pos[0]) + field_2 = crf.structured(self.pos[0]) + for i, val in enumerate(self.cond_val): + self.assertAlmostEqual(val, field_1[i], places=2) + self.assertAlmostEqual(val, field_2[(i,)], places=2) + + for dim in self.dims[1:]: + model = Model( + dim=dim, + var=0.5, + len_scale=2, + anis=[0.1, 1], + angles=[0.5, 0, 0], + ) + krige = gs.krige.Simple( + model, self.cond_pos[:dim], self.cond_val, self.mean + ) + crf = gs.CondSRF(krige, seed=19970221) + field_1 = crf.unstructured(self.pos[:dim]) + field_2 = crf.structured(self.pos[:dim]) + # check reuse + raw_kr2 = copy(crf["raw_krige"]) + crf(seed=19970222) + self.assertTrue(np.allclose(raw_kr2, crf["raw_krige"])) + for i, val in enumerate(self.cond_val): + self.assertAlmostEqual(val, field_1[i], places=2) + self.assertAlmostEqual(val, field_2[dim * (i,)], places=2) + + def test_ordinary(self): + for Model in self.cov_models: + model = Model( + dim=1, var=0.5, len_scale=2, anis=[0.1, 1], angles=[0.5, 0, 0] + ) + krige = gs.krige.Ordinary(model, self.cond_pos[0], self.cond_val) + crf = gs.CondSRF(krige, seed=19970221) + field_1 = crf.unstructured(self.pos[0]) + field_2 = crf.structured(self.pos[0]) + for i, val in enumerate(self.cond_val): + self.assertAlmostEqual(val, field_1[i], places=2) + self.assertAlmostEqual(val, field_2[(i,)], places=2) + + for dim in self.dims[1:]: + model = Model( + dim=dim, + var=0.5, + len_scale=2, + anis=[0.1, 1], + angles=[0.5, 0, 0], + ) + krige = gs.krige.Ordinary( + model, self.cond_pos[:dim], self.cond_val + ) + crf = gs.CondSRF(krige, seed=19970221) + field_1 = crf.unstructured(self.pos[:dim]) + field_2 = crf.structured(self.pos[:dim]) + for i, val in enumerate(self.cond_val): + self.assertAlmostEqual(val, field_1[i], places=2) + self.assertAlmostEqual(val, field_2[dim * (i,)], places=2) + + def test_raise_error(self): + self.assertRaises(ValueError, gs.CondSRF, gs.Gaussian()) + krige = gs.krige.Ordinary(gs.Stable(), self.cond_pos, self.cond_val) + self.assertRaises(ValueError, gs.CondSRF, krige, generator="unknown") + + def test_nugget(self): + model = gs.Gaussian( + nugget=0.01, + var=0.5, + len_scale=2, + anis=[0.1, 1], + angles=[0.5, 0, 0], + ) + krige = gs.krige.Ordinary( + model, self.cond_pos, self.cond_val, exact=True + ) + crf = gs.CondSRF(krige, seed=19970221) + field_1 = crf.unstructured(self.pos) + field_2 = crf.structured(self.pos) + for i, val in enumerate(self.cond_val): + self.assertAlmostEqual(val, field_1[i], places=2) + self.assertAlmostEqual(val, field_2[3 * (i,)], places=2) + + def test_setter(self): + krige1 = gs.krige.Krige(gs.Exponential(), self.cond_pos, self.cond_val) + krige2 = gs.krige.Krige( + gs.Gaussian(var=2), + self.cond_pos, + self.cond_val, + mean=-1, + trend=-2, + normalizer=gs.normalizer.YeoJohnson(), + ) + crf1 = gs.CondSRF(krige1) + crf2 = gs.CondSRF(krige2, seed=19970221) + # update settings + crf1.model = gs.Gaussian(var=2) + crf1.mean = -1 + crf1.trend = -2 + # also checking correctly setting uninitialized normalizer + crf1.normalizer = gs.normalizer.YeoJohnson + # check if setting went right + self.assertTrue(crf1.model == crf2.model) + self.assertTrue(crf1.normalizer == crf2.normalizer) + self.assertAlmostEqual(crf1.mean, crf2.mean) + self.assertAlmostEqual(crf1.trend, crf2.trend) + # reset kriging + crf1.krige.set_condition() + # compare fields + field1 = crf1(self.pos, seed=19970221) + field2 = crf2(self.pos) + self.assertTrue(np.all(np.isclose(field1, field2))) + + +if __name__ == "__main__": + unittest.main() diff --git a/GSTools/source/tests/test_covmodel.py b/GSTools/source/tests/test_covmodel.py new file mode 100644 index 0000000000000000000000000000000000000000..62ea8a1c258a53614e197e80be44d1ebb245ef87 --- /dev/null +++ b/GSTools/source/tests/test_covmodel.py @@ -0,0 +1,414 @@ +""" +This is the unittest of CovModel class. +""" + +import unittest + +import numpy as np + +from gstools import ( + Circular, + CovModel, + Cubic, + Exponential, + Gaussian, + HyperSpherical, + Integral, + JBessel, + Linear, + Matern, + Rational, + Spherical, + Stable, + SuperSpherical, + TPLExponential, + TPLGaussian, + TPLSimple, + TPLStable, +) +from gstools.covmodel.tools import ( + AttributeWarning, + check_arg_in_bounds, + check_bounds, +) + + +class Gau_var(CovModel): + def variogram(self, r): + h = np.abs(r) / self.len_rescaled + return self.var * (1.0 - np.exp(-(h**2))) + self.nugget + + +class Gau_cov(CovModel): + def covariance(self, r): + h = np.abs(r) / self.len_rescaled + return self.var * np.exp(-(h**2)) + + +class Gau_cor(CovModel): + def correlation(self, r): + h = np.abs(r) / self.len_rescaled + return np.exp(-(h**2)) + + +class Gau_fix(CovModel): + def cor(self, h): + return np.exp(-(h**2)) + + def fix_dim(self): + return 2 + + +class Mod_add(CovModel): + def cor(self, h): + return 1.0 + + def default_opt_arg(self): + return {"alpha": 1} + + +class TestCovModel(unittest.TestCase): + def setUp(self): + self.std_cov_models = [ + Gaussian, + Exponential, + Stable, + Rational, + Cubic, + Matern, + Linear, + Circular, + Spherical, + HyperSpherical, + SuperSpherical, + JBessel, + TPLSimple, + Integral, + ] + self.tpl_cov_models = [ + TPLGaussian, + TPLExponential, + TPLStable, + ] + self.cov_models = self.std_cov_models + self.tpl_cov_models + self.dims = range(1, 4) + self.lens = [[10, 5, 2]] + self.anis = [[0.5, 0.2]] + self.nuggets = [0, 1] + self.vars = [1, 2] + self.angles = [[1, 2, 3]] + + self.gamma_x = [1.0, 3.0, 5.0, 7.0, 9.0, 11.0] + self.gamma_y = [0.2, 0.5, 0.6, 0.8, 0.8, 0.9] + + def test_creation(self): + with self.assertRaises(TypeError): + CovModel() + + class User(CovModel): + def cor(self, h): + return np.exp(-(h**2)) + + user = User(len_scale=2) + self.assertAlmostEqual(user.correlation(1), np.exp(-0.25)) + + for Model in self.cov_models: + for dim in self.dims: + for angles in self.angles: + for nugget in self.nuggets: + for len_scale, anis in zip(self.lens, self.anis): + model = Model( + dim=dim, len_scale=len_scale, angles=angles + ) + model1 = Model( + dim=dim, len_scale=10, anis=anis, angles=angles + ) + self.assertTrue(model == model1) + self.assertAlmostEqual( + model.variogram(1), + model.var + model.nugget - model.covariance(1), + ) + self.assertAlmostEqual( + model.covariance(1), + model.var * model.correlation(1), + ) + self.assertAlmostEqual( + model.covariance(1), + model.var * model.correlation(1), + ) + self.assertAlmostEqual( + model.vario_spatial(([1], [2], [3])[:dim])[0], + model.var + + model.nugget + - model.cov_spatial(([1], [2], [3])[:dim])[0], + ) + self.assertAlmostEqual( + model.cor_spatial(([1], [2], [3])[:dim])[0], + model.cov_spatial(([1], [2], [3])[:dim])[0] + / model.var, + ) + for d in range(dim): + self.assertAlmostEqual( + model.vario_axis(1, axis=d), + model.var + + model.nugget + - model.cov_axis(1, axis=d), + ) + self.assertAlmostEqual( + model.cor_axis(1, axis=d), + model.cov_axis(1, axis=d) / model.var, + ) + self.assertAlmostEqual( + model.cov_nugget(0), model.sill + ) + self.assertAlmostEqual(model.vario_nugget(0), 0.0) + self.assertAlmostEqual( + model.cov_nugget(1), model.covariance(1) + ) + self.assertAlmostEqual(model.vario_nugget(0), 0.0) + self.assertAlmostEqual( + model.vario_nugget(1), model.variogram(1) + ) + # check if callable + model.vario_spatial((1, 2, 3)[:dim]) + model.spectral_density([0, 1]) + model.spectrum([0, 1]) + model.spectral_rad_pdf([0, 1]) + model.ln_spectral_rad_pdf([0, 1]) + model.integral_scale_vec + model.percentile_scale(0.9) + if model.has_cdf: + model.spectral_rad_cdf([0, 1]) + if model.has_ppf: + model.spectral_rad_ppf([0.0, 0.99]) + model.pykrige_kwargs + # check arg bound setting + model.set_arg_bounds( + var=[2, np.inf], nugget=[1, 2] + ) + self.assertAlmostEqual(model.var, 3) + self.assertAlmostEqual(model.nugget, 1.5) + + def test_tpl_models(self): + for Model in self.tpl_cov_models: + for dim in self.dims: + model = Model(dim=dim, len_scale=9, len_low=1, rescale=2) + self.assertAlmostEqual(model.len_up_rescaled, 5) + model.len_low = 0.0 + self.assertAlmostEqual(model.cor(2), model.correlation(9)) + # also check resetting of var when sill is given lower + model.fit_variogram( + self.gamma_x, self.gamma_y, sill=1.1, nugget=False + ) + self.assertAlmostEqual(model.var, 1.1, delta=1e-5) + # integral scale is not setable when len_low is not 0 + with self.assertRaises(ValueError): + Model(len_low=5, integral_scale=10) + + def test_fitting(self): + for Model in self.std_cov_models: + for dim in self.dims: + model = Model(dim=dim) + model.fit_variogram(self.gamma_x, self.gamma_y, nugget=False) + self.assertAlmostEqual(model.nugget, 0.0) + model = Model(dim=dim) + # also check resetting of var when sill is given lower + model.fit_variogram(self.gamma_x, self.gamma_y, sill=0.9) + self.assertAlmostEqual(model.nugget + model.var, 0.9) + model = Model(dim=dim) + # more detailed checks + model.fit_variogram( + self.gamma_x, self.gamma_y, sill=2, nugget=False + ) + self.assertAlmostEqual(model.var, 2.0) + model = Model(dim=dim) + model.fit_variogram( + self.gamma_x, self.gamma_y, sill=2, nugget=1 + ) + self.assertAlmostEqual(model.var, 1) + model = Model(dim=dim) + ret = model.fit_variogram( + self.gamma_x, + self.gamma_y, + loss="linear", + return_r2=True, + weights="inv", + init_guess="current", + ) + self.assertEqual(len(ret), 3) + + # treatment of sill/var/nugget by fitting + model = Stable() + with self.assertRaises(ValueError): + model.fit_variogram( + self.gamma_x, self.gamma_y, nugget=False, var=False, sill=2 + ) + model.fit_variogram(self.gamma_x, self.gamma_y, var=2, sill=3) + self.assertAlmostEqual(model.var, 2) + self.assertAlmostEqual(model.nugget, 1) + model.var = 3 + with self.assertRaises(ValueError): + model.fit_variogram(self.gamma_x, self.gamma_y, var=False, sill=2) + model.fit_variogram(self.gamma_x, self.gamma_y, weights="inv") + len_save = model.len_scale + model.fit_variogram( + self.gamma_x, self.gamma_y, weights=lambda x: 1 / (1 + x) + ) + self.assertAlmostEqual(model.len_scale, len_save, places=6) + # check ValueErrors + with self.assertRaises(ValueError): + model.fit_variogram(self.gamma_x, self.gamma_y, sill=2, var=3) + with self.assertRaises(ValueError): + model.fit_variogram(self.gamma_x, self.gamma_y, sill=2, nugget=3) + with self.assertRaises(ValueError): + model.fit_variogram(self.gamma_x, self.gamma_y, method="wrong") + with self.assertRaises(ValueError): + model.fit_variogram(self.gamma_x, self.gamma_y, wrong=False) + model.var_bounds = [0, 1] + model.nugget_bounds = [0, 1] + with self.assertRaises(ValueError): + model.fit_variogram(self.gamma_x, self.gamma_y, sill=3) + # init guess + with self.assertRaises(ValueError): + model.fit_variogram(self.gamma_x, self.gamma_y, init_guess="wrong") + with self.assertRaises(ValueError): + model.fit_variogram( + self.gamma_x, self.gamma_y, init_guess={"wrong": 1} + ) + # sill fixing + model.var_bounds = [0, np.inf] + model.fit_variogram( + self.gamma_x, np.array(self.gamma_y) + 1, sill=2, alpha=False + ) + self.assertAlmostEqual(model.var + model.nugget, 2) + # check isotropicity for latlon models + model = Stable(latlon=True) + with self.assertRaises(ValueError): + model.fit_variogram(self.gamma_x, 3 * [self.gamma_y]) + + def test_covmodel_class(self): + model_std = Gaussian(rescale=3, var=1.1, nugget=1.2, len_scale=1.3) + model_var = Gau_var(rescale=3, var=1.1, nugget=1.2, len_scale=1.3) + model_cov = Gau_cov(rescale=3, var=1.1, nugget=1.2, len_scale=1.3) + model_cor = Gau_cor(rescale=3, var=1.1, nugget=1.2, len_scale=1.3) + var = model_std.variogram(2.5) + cov = model_std.covariance(2.5) + corr = model_std.correlation(2.5) + cor = model_std.cor(2.5) + + self.assertFalse(check_bounds(bounds=[0])) + self.assertFalse(check_bounds(bounds=[1, -1])) + self.assertFalse(check_bounds(bounds=[0, 1, 2, 3])) + self.assertFalse(check_bounds(bounds=[0, 1, "kk"])) + self.assertRaises(ValueError, model_std.set_arg_bounds, wrong_arg=[1]) + self.assertRaises( + ValueError, model_std.set_arg_bounds, wrong_arg=[-1, 1] + ) + + # checking some properties + model_par = Stable() + self.assertFalse(model_par.do_rotation) + self.assertEqual(len(model_par.arg), len(model_par.arg_list)) + self.assertEqual(len(model_par.iso_arg), len(model_par.iso_arg_list)) + self.assertEqual(len(model_par.arg), len(model_par.iso_arg) + 2) + self.assertEqual(len(model_par.len_scale_vec), model_par.dim) + self.assertFalse(Gaussian() == Stable()) + model_par.hankel_kw = {"N": 300} + self.assertEqual(model_par.hankel_kw["N"], 300) + + # arg in bounds check + model_std.set_arg_bounds(var=[0.5, 1.5]) + with self.assertRaises(ValueError): + model_std.var = 0.4 + with self.assertRaises(ValueError): + model_std.var = 1.6 + model_std.set_arg_bounds(var=[0.5, 1.5, "oo"]) + with self.assertRaises(ValueError): + model_std.var = 0.5 + with self.assertRaises(ValueError): + model_std.var = 1.5 + with self.assertRaises(ValueError): + model_std.var_bounds = [1, -1] + with self.assertRaises(ValueError): + model_std.len_scale_bounds = [1, -1] + with self.assertRaises(ValueError): + model_std.nugget_bounds = [1, -1] + with self.assertRaises(ValueError): + model_std.anis_bounds = [1, -1] + # reset the standard model + model_std = Gaussian(rescale=3, var=1.1, nugget=1.2, len_scale=1.3) + # std value from bounds with neg. inf and finit bound + model_add = Mod_add() + model_add.set_arg_bounds(alpha=[-np.inf, 0]) + self.assertAlmostEqual(model_add.alpha, -1) + # special treatment of anis check + model_std.set_arg_bounds(anis=[2, 4, "oo"]) + self.assertTrue(np.all(np.isclose(model_std.anis, 3))) + # dim specific checks + with self.assertWarns(AttributeWarning): + Gau_fix(dim=1) + self.assertRaises(ValueError, Gaussian, dim=0) + self.assertRaises(ValueError, Gau_fix, latlon=True) + # check inputs + self.assertRaises(ValueError, model_std.percentile_scale, per=-1.0) + self.assertRaises(ValueError, Gaussian, anis=-1.0) + self.assertRaises(ValueError, Gaussian, len_scale=[1, -1]) + self.assertRaises(ValueError, check_arg_in_bounds, model_std, "wrong") + self.assertWarns(AttributeWarning, Gaussian, wrong_arg=1.0) + with self.assertWarns(AttributeWarning): + self.assertRaises(ValueError, Gaussian, len_rescaled=1.0) + + # check correct subclassing + with self.assertRaises(TypeError): + + class Gau_err(CovModel): + pass + + self.assertAlmostEqual(var, model_var.variogram(2.5)) + self.assertAlmostEqual(var, model_cov.variogram(2.5)) + self.assertAlmostEqual(var, model_cor.variogram(2.5)) + self.assertAlmostEqual(cov, model_var.covariance(2.5)) + self.assertAlmostEqual(cov, model_cov.covariance(2.5)) + self.assertAlmostEqual(cov, model_cor.covariance(2.5)) + self.assertAlmostEqual(corr, model_var.correlation(2.5)) + self.assertAlmostEqual(corr, model_cov.correlation(2.5)) + self.assertAlmostEqual(corr, model_cor.correlation(2.5)) + self.assertAlmostEqual(cor, model_var.cor(2.5)) + self.assertAlmostEqual(cor, model_cov.cor(2.5)) + self.assertAlmostEqual(cor, model_cor.cor(2.5)) + + def test_rescale(self): + model1 = Exponential() + model2 = Exponential(rescale=2.1) + model3 = Exponential(rescale=2.1, len_scale=2.1) + + self.assertAlmostEqual( + model1.integral_scale, 2.1 * model2.integral_scale + ) + self.assertAlmostEqual(model1.integral_scale, model3.integral_scale) + + def test_special_models(self): + # Matern and Integral converge to gaussian + model0 = Integral(rescale=0.5) + model0.set_arg_bounds(nu=[0, 1001]) + model0.nu = 1000 + model1 = Matern() + model1.set_arg_bounds(nu=[0, 101]) + model1.nu = 100 + model2 = Gaussian(rescale=0.5) + self.assertAlmostEqual(model0.variogram(1), model2.variogram(1), 2) + self.assertAlmostEqual(model0.spectrum(1), model2.spectrum(1), 2) + self.assertAlmostEqual(model1.variogram(1), model2.variogram(1)) + self.assertAlmostEqual(model1.spectrum(1), model2.spectrum(1), 2) + # stable model gets unstable for alpha < 0.3 + with self.assertWarns(AttributeWarning): + Stable(alpha=0.2) + with self.assertWarns(AttributeWarning): + TPLStable(alpha=0.2) + # corner case for JBessel model + with self.assertWarns(AttributeWarning): + JBessel(dim=3, nu=0.5) + + +if __name__ == "__main__": + unittest.main() diff --git a/GSTools/source/tests/test_export.py b/GSTools/source/tests/test_export.py new file mode 100644 index 0000000000000000000000000000000000000000..b32898f4527d0da45112366a28b099d15ef5f3f1 --- /dev/null +++ b/GSTools/source/tests/test_export.py @@ -0,0 +1,64 @@ +"""Test the PyVista/VTK export methods""" + +import os +import shutil +import tempfile +import unittest + +import numpy as np + +from gstools import SRF, Exponential, Gaussian +from gstools.random import MasterRNG + +HAS_PYVISTA = False +try: + import pyvista as pv + + HAS_PYVISTA = True +except ImportError: + pass + + +class TestExport(unittest.TestCase): + def setUp(self): + self.test_dir = tempfile.mkdtemp() + # structured field with a size 100x100x100 and a grid-size of 1x1x1 + x = y = z = range(50) + model = Gaussian(dim=3, var=0.6, len_scale=20) + self.srf_structured = SRF(model) + self.srf_structured((x, y, z), mesh_type="structured") + # unstrucutred field + seed = MasterRNG(19970221) + rng = np.random.RandomState(seed()) + x = rng.randint(0, 100, size=1000) + y = rng.randint(0, 100, size=1000) + model = Exponential( + dim=2, var=1, len_scale=[12.0, 3.0], angles=np.pi / 8.0 + ) + self.srf_unstructured = SRF(model, seed=20170519) + self.srf_unstructured([x, y]) + + def tearDown(self): + # Remove the test data directory after the test + shutil.rmtree(self.test_dir) + + @unittest.skipIf(not HAS_PYVISTA, "PyVista is not installed.") + def test_pyvista(self): + mesh = self.srf_structured.to_pyvista() + self.assertIsInstance(mesh, pv.RectilinearGrid) + mesh = self.srf_unstructured.to_pyvista() + self.assertIsInstance(mesh, pv.UnstructuredGrid) + + def test_pyevtk_export(self): + # Structured + sfilename = os.path.join(self.test_dir, "structured") + self.srf_structured.vtk_export(sfilename) + self.assertTrue(os.path.isfile(sfilename + ".vtr")) + # Unstructured + ufilename = os.path.join(self.test_dir, "unstructured") + self.srf_unstructured.vtk_export(ufilename) + self.assertTrue(os.path.isfile(ufilename + ".vtu")) + + +if __name__ == "__main__": + unittest.main() diff --git a/GSTools/source/tests/test_field.py b/GSTools/source/tests/test_field.py new file mode 100644 index 0000000000000000000000000000000000000000..cdb0365d638adbe205f76b8d3924050809d85e9c --- /dev/null +++ b/GSTools/source/tests/test_field.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python +""" +This is the unittest of SRF class. +""" + +import unittest + +import numpy as np + +import gstools as gs + + +class TestField(unittest.TestCase): + def setUp(self): + self.cov_model = gs.Gaussian(dim=2, var=1.5, len_scale=4.0) + rng = np.random.RandomState(123018) + x = rng.uniform(0.0, 10, 100) + y = rng.uniform(0.0, 10, 100) + self.field = rng.uniform(0.0, 10, 100) + self.pos = np.array([x, y]) + + def test_standalone(self): + fld = gs.field.Field(dim=2) + fld_cov = gs.field.Field(model=self.cov_model) + field1 = fld(self.pos, self.field) + field2 = fld_cov(self.pos, self.field) + self.assertTrue(np.all(np.isclose(field1, field2))) + self.assertTrue(np.all(np.isclose(field1, self.field))) + + def test_raise(self): + # vector field on latlon + fld = gs.field.Field(gs.Gaussian(latlon=True), value_type="vector") + self.assertRaises(ValueError, fld, [1, 2], [1, 2]) + # no pos tuple present + fld = gs.field.Field(dim=2) + self.assertRaises(ValueError, fld.post_field, [1, 2]) + # wrong model type + with self.assertRaises(ValueError): + gs.field.Field(model=3.1415) + # no model and no dim given + with self.assertRaises(ValueError): + gs.field.Field() + # wrong value type + with self.assertRaises(ValueError): + gs.field.Field(dim=2, value_type="complex") + # wrong mean shape + with self.assertRaises(ValueError): + gs.field.Field(dim=3, mean=[1, 2]) + + def test_pos_compare(self): + fld = gs.field.Field(dim=1) + fld.set_pos([1, 2]) + fld._dim = 2 + info = fld.set_pos([[1], [2]], info=True) + self.assertTrue(info["deleted"]) + info = fld.set_pos([[2], [3]], info=True) + self.assertTrue(info["deleted"]) + + def test_magic(self): + fld = gs.field.Field(dim=1) + f1 = np.array([0, 0], dtype=np.double) + f2 = np.array([2, 3], dtype=np.double) + fld([1, 2], store="f1") # default field with zeros + fld([1, 2], f2, store="f2") + fields1 = fld[:] + fields2 = fld[[0, 1]] + fields3 = fld[["f1", "f2"]] + fields4 = fld.all_fields + self.assertTrue(np.allclose([f1, f2], fields1)) + self.assertTrue(np.allclose([f1, f2], fields2)) + self.assertTrue(np.allclose([f1, f2], fields3)) + self.assertTrue(np.allclose([f1, f2], fields4)) + self.assertEqual(len(fld), 2) + self.assertTrue("f1" in fld) + self.assertTrue("f2" in fld) + self.assertFalse("f3" in fld) + # subscription + with self.assertRaises(KeyError): + fld["f3"] + with self.assertRaises(KeyError): + del fld["f3"] + with self.assertRaises(KeyError): + del fld[["f3"]] + del fld["f1"] + self.assertFalse("f1" in fld) + fld([1, 2], f1, store="f1") + del fld[-1] + self.assertFalse("f1" in fld) + fld([1, 2], f1, store="f1") + del fld[:] + self.assertEqual(len(fld), 0) + fld([1, 2], f1, store="f1") + del fld.field_names + self.assertEqual(len(fld), 0) + # store config (missing check) + name, save = fld.get_store_config(store="fld", fld_cnt=1) + self.assertEqual(name, ["fld"]) + self.assertTrue(save[0]) + + def test_reuse(self): + fld = gs.field.Field(dim=1) + # no pos tuple + with self.assertRaises(ValueError): + fld() + # no field shape + with self.assertRaises(ValueError): + fld.post_field([1, 2]) + # bad name + fld.set_pos([1, 2]) + with self.assertRaises(ValueError): + fld.post_field([1, 2], process=False, name=0) + # incompatible reuse + with self.assertRaises(ValueError): + fld.structured() + fld.set_pos([1, 2], "structured") + with self.assertRaises(ValueError): + fld.unstructured() + + +if __name__ == "__main__": + unittest.main() diff --git a/GSTools/source/tests/test_fouriergen.py b/GSTools/source/tests/test_fouriergen.py new file mode 100644 index 0000000000000000000000000000000000000000..093b47d5a92d29cb6bf58d32559e1b472b98a975 --- /dev/null +++ b/GSTools/source/tests/test_fouriergen.py @@ -0,0 +1,111 @@ +""" +This is the unittest of the Fourier class. +""" + +import unittest + +import numpy as np + +import gstools as gs + + +class TestFourier(unittest.TestCase): + def setUp(self): + self.seed = 19900408 + self.cov_model_1d = gs.Gaussian(dim=1, var=0.5, len_scale=10.0) + self.cov_model_2d = gs.Gaussian(dim=2, var=2.0, len_scale=30.0) + self.cov_model_3d = gs.Gaussian(dim=3, var=2.1, len_scale=21.0) + self.L = [80, 30, 91] + self.x = np.linspace(0, self.L[0], 11) + self.y = np.linspace(0, self.L[1], 31) + self.z = np.linspace(0, self.L[2], 13) + + self.mode_no = [12, 6, 14] + + self.srf_1d = gs.SRF( + self.cov_model_1d, + generator="Fourier", + mode_no=[self.mode_no[0]], + period=[self.L[0]], + seed=self.seed, + ) + self.srf_2d = gs.SRF( + self.cov_model_2d, + generator="Fourier", + mode_no=self.mode_no[:2], + period=self.L[:2], + seed=self.seed, + ) + self.srf_3d = gs.SRF( + self.cov_model_3d, + generator="Fourier", + mode_no=self.mode_no, + period=self.L, + seed=self.seed, + ) + + def test_1d(self): + field = self.srf_1d((self.x,), mesh_type="structured") + self.assertAlmostEqual(field[0], 0.6236929351309081) + + def test_2d(self): + field = self.srf_2d((self.x, self.y), mesh_type="structured") + self.assertAlmostEqual(field[0, 0], -0.1431996611581266) + + def test_3d(self): + field = self.srf_3d((self.x, self.y, self.z), mesh_type="structured") + self.assertAlmostEqual(field[0, 0, 0], -1.0433325279452803) + + def test_periodicity_1d(self): + field = self.srf_1d((self.x,), mesh_type="structured") + self.assertAlmostEqual(field[0], field[-1]) + + def test_periodicity_2d(self): + field = self.srf_2d((self.x, self.y), mesh_type="structured") + self.assertAlmostEqual( + field[0, len(self.y) // 2], field[-1, len(self.y) // 2] + ) + self.assertAlmostEqual( + field[len(self.x) // 2, 0], field[len(self.x) // 2, -1] + ) + + def test_periodicity_3d(self): + field = self.srf_3d((self.x, self.y, self.z), mesh_type="structured") + self.assertAlmostEqual( + field[0, len(self.y) // 2, 0], field[-1, len(self.y) // 2, 0] + ) + self.assertAlmostEqual(field[0, 0, 0], field[0, -1, 0]) + self.assertAlmostEqual( + field[len(self.x) // 2, len(self.y) // 2, 0], + field[len(self.x) // 2, len(self.y) // 2, -1], + ) + + def test_setters(self): + new_period = [5, 10] + self.srf_2d.generator.period = new_period + np.testing.assert_almost_equal( + self.srf_2d.generator.period, + np.array(new_period), + ) + new_mode_no = [6, 6] + self.srf_2d.generator.mode_no = new_mode_no + np.testing.assert_almost_equal( + self.srf_2d.generator.mode_no, + np.array(new_mode_no), + ) + + def test_assertions(self): + # unstructured grids not supported + self.assertRaises(ValueError, self.srf_2d, (self.x, self.y)) + self.assertRaises( + ValueError, self.srf_2d, (self.x, self.y), mesh_type="unstructured" + ) + self.assertRaises( + ValueError, + gs.SRF, + self.cov_model_2d, + generator="Fourier", + mode_no=[13, 50], + period=self.L[:2], + seed=self.seed, + ) diff --git a/GSTools/source/tests/test_incomprrandmeth.py b/GSTools/source/tests/test_incomprrandmeth.py new file mode 100644 index 0000000000000000000000000000000000000000..9156b1b175a60079b3426f941677c68138606566 --- /dev/null +++ b/GSTools/source/tests/test_incomprrandmeth.py @@ -0,0 +1,63 @@ +""" +This is the unittest of the RandMeth class. +""" + +import copy +import unittest + +import numpy as np + +import gstools as gs +from gstools.field.generator import IncomprRandMeth + + +class TestIncomprRandMeth(unittest.TestCase): + def setUp(self): + self.cov_model_2d = gs.Gaussian(dim=2, var=1.5, len_scale=2.5) + self.cov_model_3d = copy.deepcopy(self.cov_model_2d) + self.cov_model_3d.dim = 3 + self.seed = 19031977 + self.x_grid = np.linspace(0.0, 10.0, 9) + self.y_grid = np.linspace(-5.0, 5.0, 16) + self.z_grid = np.linspace(-6.0, 7.0, 8) + self.x_tuple = np.linspace(0.0, 10.0, 10) + self.y_tuple = np.linspace(-5.0, 5.0, 10) + self.z_tuple = np.linspace(-6.0, 8.0, 10) + + self.rm_2d = IncomprRandMeth( + self.cov_model_2d, mode_no=100, seed=self.seed + ) + self.rm_3d = IncomprRandMeth( + self.cov_model_3d, mode_no=100, seed=self.seed + ) + + def test_unstruct_2d(self): + modes = self.rm_2d((self.x_tuple, self.y_tuple)) + self.assertAlmostEqual(modes[0, 0], 0.50751115) + self.assertAlmostEqual(modes[0, 1], 1.03291018) + self.assertAlmostEqual(modes[1, 1], -0.22003005) + + def test_unstruct_3d(self): + modes = self.rm_3d((self.x_tuple, self.y_tuple, self.z_tuple)) + self.assertAlmostEqual(modes[0, 0], 0.7924546333550331) + self.assertAlmostEqual(modes[0, 1], 1.660747056686244) + self.assertAlmostEqual(modes[1, 0], -0.28049855754819514) + + def test_assertions(self): + cov_model_1d = gs.Gaussian(dim=1, var=1.5, len_scale=2.5) + self.assertRaises(ValueError, IncomprRandMeth, cov_model_1d) + + def test_vector_mean(self): + srf = gs.SRF( + self.cov_model_2d, + mean=(0.5, 0), + generator="VectorField", + seed=198412031, + ) + srf.structured((self.x_grid, self.y_grid)) + self.assertAlmostEqual(np.mean(srf.field[0]), 1.3025621393180298) + self.assertAlmostEqual(np.mean(srf.field[1]), -0.04729596839446052) + + +if __name__ == "__main__": + unittest.main() diff --git a/GSTools/source/tests/test_krige.py b/GSTools/source/tests/test_krige.py new file mode 100644 index 0000000000000000000000000000000000000000..d702b0eef3276c44d1cee4b2c74a554eea8d8ba9 --- /dev/null +++ b/GSTools/source/tests/test_krige.py @@ -0,0 +1,291 @@ +""" +This is the unittest of the kriging module. +""" + +import unittest + +import numpy as np + +import gstools as gs + + +def trend(*xyz): + return xyz[0] + + +def mean_func(*xyz): + return 2 * xyz[0] + + +class TestKrige(unittest.TestCase): + def setUp(self): + self.cov_models = [gs.Gaussian, gs.Exponential, gs.Spherical] + self.dims = range(1, 4) + self.data = np.array( + [ + [0.3, 1.2, 0.5, 0.47], + [1.9, 0.6, 1.0, 0.56], + [1.1, 3.2, 1.5, 0.74], + [3.3, 4.4, 2.0, 1.47], + [4.7, 3.8, 2.5, 1.74], + ] + ) + # redundant data for pseudo-inverse + self.p_data = np.zeros((3, 3)) + self.p_vals = np.array([1.0, 2.0, 6.0]) + self.p_meth = [1, 2, 3] # method selector + # indices for the date in the grid + self.data_idx = tuple(np.array(self.data[:, :3] * 10, dtype=int).T) + # x, y, z componentes for the conditon position + self.cond_pos = (self.data[:, 0], self.data[:, 1], self.data[:, 2]) + # condition values + self.cond_val = self.data[:, 3] + self.cond_err = np.array([0.01, 0.0, 0.1, 0.05, 0]) + # the arithmetic mean of the conditions + self.mean = np.mean(self.cond_val) + # the grid + self.x = np.linspace(0, 5, 51) + self.y = np.linspace(0, 6, 61) + self.z = np.linspace(0, 7, 71) + self.pos = (self.x, self.y, self.z) + self.grids = [self.x] + self.grids.append(np.meshgrid(self.x, self.y, indexing="ij")) + self.grids.append(np.meshgrid(self.x, self.y, self.z, indexing="ij")) + self.grid_shape = [51, 61, 71] + + def test_simple(self): + for Model in self.cov_models: + for dim in self.dims: + model = Model( + dim=dim, + var=2, + len_scale=2, + anis=[0.9, 0.8], + angles=[2, 1, 0.5], + ) + simple = gs.krige.Simple( + model, self.cond_pos[:dim], self.cond_val, self.mean + ) + field_1, __ = simple.unstructured(self.grids[dim - 1]) + field_1 = field_1.reshape(self.grid_shape[:dim]) + field_2, __ = simple.structured(self.pos[:dim]) + self.assertAlmostEqual( + np.max(np.abs(field_1 - field_2)), 0.0, places=2 + ) + for i, val in enumerate(self.cond_val): + self.assertAlmostEqual( + field_1[self.data_idx[:dim]][i], val, places=2 + ) + + def test_ordinary(self): + for trend_func in [None, trend]: + for Model in self.cov_models: + for dim in self.dims: + model = Model( + dim=dim, + var=5, + len_scale=10, + anis=[0.9, 0.8], + angles=[2, 1, 0.5], + ) + ordinary = gs.krige.Ordinary( + model, + self.cond_pos[:dim], + self.cond_val, + trend=trend_func, + ) + field_1, __ = ordinary.unstructured(self.grids[dim - 1]) + field_1 = field_1.reshape(self.grid_shape[:dim]) + field_2, __ = ordinary.structured(self.pos[:dim]) + self.assertAlmostEqual( + np.max(np.abs(field_1 - field_2)), 0.0, places=2 + ) + for i, val in enumerate(self.cond_val): + self.assertAlmostEqual( + field_1[self.data_idx[:dim]][i], val, places=2 + ) + + def test_universal(self): + # "quad" -> to few conditional points + for drift in ["linear", 0, 1, trend]: + for Model in self.cov_models: + for dim in self.dims: + model = Model( + dim=dim, + var=2, + len_scale=10, + anis=[0.9, 0.8], + angles=[2, 1, 0.5], + ) + universal = gs.krige.Universal( + model, self.cond_pos[:dim], self.cond_val, drift + ) + field_1, __ = universal.unstructured(self.grids[dim - 1]) + field_1 = field_1.reshape(self.grid_shape[:dim]) + field_2, __ = universal.structured(self.pos[:dim]) + self.assertAlmostEqual( + np.max(np.abs(field_1 - field_2)), 0.0, places=2 + ) + for i, val in enumerate(self.cond_val): + self.assertAlmostEqual( + field_2[self.data_idx[:dim]][i], val, places=2 + ) + + def test_detrended(self): + for Model in self.cov_models: + for dim in self.dims: + model = Model( + dim=dim, + var=2, + len_scale=10, + anis=[0.5, 0.2], + angles=[0.4, 0.2, 0.1], + ) + detrended = gs.krige.Detrended( + model, self.cond_pos[:dim], self.cond_val, trend + ) + field_1, __ = detrended.unstructured(self.grids[dim - 1]) + field_1 = field_1.reshape(self.grid_shape[:dim]) + field_2, __ = detrended.structured(self.pos[:dim]) + # detrended.plot() + self.assertAlmostEqual( + np.max(np.abs(field_1 - field_2)), 0.0, places=2 + ) + for i, val in enumerate(self.cond_val): + self.assertAlmostEqual( + field_2[self.data_idx[:dim]][i], val, places=2 + ) + + def test_extdrift(self): + ext_drift = [] + cond_drift = [] + for i, grid in enumerate(self.grids): + dim = i + 1 + model = gs.Exponential( + dim=dim, + var=2, + len_scale=10, + anis=[0.9, 0.8], + angles=[2, 1, 0.5], + ) + srf = gs.SRF(model) + field = srf(grid) + ext_drift.append(field) + field = field.reshape(self.grid_shape[:dim]) + cond_drift.append(field[self.data_idx[:dim]]) + + for Model in self.cov_models: + for dim in self.dims: + model = Model( + dim=dim, + var=2, + len_scale=10, + anis=[0.5, 0.2], + angles=[0.4, 0.2, 0.1], + ) + extdrift = gs.krige.ExtDrift( + model, + self.cond_pos[:dim], + self.cond_val, + cond_drift[dim - 1], + ) + field_1, __ = extdrift.unstructured( + self.grids[dim - 1], ext_drift=ext_drift[dim - 1] + ) + field_1 = field_1.reshape(self.grid_shape[:dim]) + field_2, __ = extdrift.structured( + self.pos[:dim], ext_drift=ext_drift[dim - 1] + ) + # extdrift.plot() + self.assertAlmostEqual( + np.max(np.abs(field_1 - field_2)), 0.0, places=2 + ) + for i, val in enumerate(self.cond_val): + self.assertAlmostEqual( + field_2[self.data_idx[:dim]][i], val, places=2 + ) + + def test_pseudo(self): + for Model in self.cov_models: + for dim in self.dims: + model = Model( + dim=dim, + var=2, + len_scale=10, + anis=[0.5, 0.2], + angles=[0.4, 0.2, 0.1], + ) + for meth in self.p_meth: + krig = gs.krige.Krige( + model, self.p_data[:dim], self.p_vals, unbiased=False + ) + field, __ = krig([0, 0, 0][:dim]) + # with the pseudo-inverse, the estimated value + # should be the mean of the 3 redundant input values + self.assertAlmostEqual( + field[0], np.mean(self.p_vals), places=2 + ) + + def test_error(self): + for Model in self.cov_models: + for dim in self.dims: + model = Model( + dim=dim, + var=5, + len_scale=10, + nugget=0.1, + anis=[0.9, 0.8], + angles=[2, 1, 0.5], + ) + ordinary = gs.krige.Ordinary( + model, + self.cond_pos[:dim], + self.cond_val, + exact=False, + cond_err=self.cond_err, + ) + field, err = ordinary(self.cond_pos[:dim]) + # when the given measurement error is 0, the kriging-var + # should equal the nugget of the model + self.assertAlmostEqual(err[1], model.nugget, places=2) + self.assertAlmostEqual(err[4], model.nugget, places=2) + + def test_raise(self): + # no cond_pos/cond_val given + self.assertRaises(ValueError, gs.krige.Krige, gs.Stable(), None, None) + + def test_krige_mean(self): + # check for constant mean (simple kriging) + krige = gs.krige.Simple(gs.Gaussian(), self.cond_pos, self.cond_val) + mean_f = krige.structured(self.pos, only_mean=True) + self.assertTrue(np.all(np.isclose(mean_f, 0))) + krige = gs.krige.Simple( + gs.Gaussian(), + self.cond_pos, + self.cond_val, + mean=mean_func, + normalizer=gs.normalizer.YeoJohnson, + trend=trend, + ) + # check applying mean, norm, trend + mean_f1 = krige.structured(self.pos, only_mean=True) + mean_f2 = gs.normalizer.tools.apply_mean_norm_trend( + self.pos, + np.zeros(tuple(map(len, self.pos))), + mean=mean_func, + normalizer=gs.normalizer.YeoJohnson, + trend=trend, + mesh_type="structured", + ) + self.assertTrue(np.all(np.isclose(mean_f1, mean_f2))) + krige = gs.krige.Simple(gs.Gaussian(), self.cond_pos, self.cond_val) + mean_f = krige.structured(self.pos, only_mean=True) + self.assertTrue(np.all(np.isclose(mean_f, 0))) + # check for constant mean (ordinary kriging) + krige = gs.krige.Ordinary(gs.Gaussian(), self.cond_pos, self.cond_val) + mean_f = krige.structured(self.pos, only_mean=True) + self.assertTrue(np.all(np.isclose(mean_f, krige.get_mean()))) + + +if __name__ == "__main__": + unittest.main() diff --git a/GSTools/source/tests/test_latlon.py b/GSTools/source/tests/test_latlon.py new file mode 100644 index 0000000000000000000000000000000000000000..98088db85e542e44cfb72c72690dad28882097a9 --- /dev/null +++ b/GSTools/source/tests/test_latlon.py @@ -0,0 +1,172 @@ +""" +This is the unittest for latlon related routines. +""" + +import unittest + +import numpy as np + +import gstools as gs + + +def _rel_err(a, b): + return np.abs(a / ((a + b) / 2) - 1) + + +class ErrMod(gs.CovModel): + def cor(self, h): + return np.exp(-(h**2)) + + def fix_dim(self): + return 2 + + +class TestLatLon(unittest.TestCase): + def setUp(self): + self.cmod = gs.Gaussian( + latlon=True, var=2, len_scale=777, geo_scale=gs.KM_SCALE + ) + self.lat = self.lon = range(-80, 81) + + self.data = np.array( + [ + [52.9336, 8.237, 15.7], + [48.6159, 13.0506, 13.9], + [52.4853, 7.9126, 15.1], + [50.7446, 9.345, 17.0], + [52.9437, 12.8518, 21.9], + [53.8633, 8.1275, 11.9], + [47.8342, 10.8667, 11.4], + [51.0881, 12.9326, 17.2], + [48.406, 11.3117, 12.9], + [49.7273, 8.1164, 17.2], + [49.4691, 11.8546, 13.4], + [48.0197, 12.2925, 13.9], + [50.4237, 7.4202, 18.1], + [53.0316, 13.9908, 21.3], + [53.8412, 13.6846, 21.3], + [54.6792, 13.4343, 17.4], + [49.9694, 9.9114, 18.6], + [51.3745, 11.292, 20.2], + [47.8774, 11.3643, 12.7], + [50.5908, 12.7139, 15.8], + ] + ) + + def test_conv(self): + p_ll = gs.tools.geometric.latlon2pos((self.lat, self.lon), 2.56) + ll_p = gs.tools.geometric.pos2latlon(p_ll, 2.56) + for i, v in enumerate(self.lat): + self.assertAlmostEqual(v, ll_p[0, i]) + self.assertAlmostEqual(v, ll_p[1, i]) + self.assertAlmostEqual( + 8, self.cmod.anisometrize(self.cmod.isometrize((8, 6)))[0, 0] + ) + self.assertAlmostEqual( + 6, self.cmod.anisometrize(self.cmod.isometrize((8, 6)))[1, 0] + ) + self.assertAlmostEqual( + gs.EARTH_RADIUS, + self.cmod.isometrize( + self.cmod.anisometrize((gs.EARTH_RADIUS, 0, 0)) + )[0, 0], + ) + + def test_cov_model(self): + self.assertAlmostEqual( + self.cmod.vario_yadrenko(1.234), + self.cmod.sill - self.cmod.cov_yadrenko(1.234), + ) + self.assertAlmostEqual( + self.cmod.cov_yadrenko(1.234), + self.cmod.var * self.cmod.cor_yadrenko(1.234), + ) + # test if correctly handling tries to set anisotropy + self.cmod.anis = [1, 2] + self.cmod.angles = [1, 2, 3] + self.assertAlmostEqual(self.cmod.anis[0], 1) + self.assertAlmostEqual(self.cmod.anis[1], 1) + self.assertAlmostEqual(self.cmod.angles[0], 0) + self.assertAlmostEqual(self.cmod.angles[1], 0) + self.assertAlmostEqual(self.cmod.angles[2], 0) + + def test_vario_est(self): + srf = gs.SRF(self.cmod, seed=12345) + field = srf.structured((self.lat, self.lon)) + + bin_edges = np.linspace(0, 3 * 777, 30) + bin_center, emp_vario = gs.vario_estimate( + *((self.lat, self.lon), field, bin_edges), + latlon=True, + mesh_type="structured", + sampling_size=2000, + sampling_seed=12345, + geo_scale=gs.KM_SCALE, + ) + mod = gs.Gaussian(latlon=True, geo_scale=gs.KM_SCALE) + mod.fit_variogram(bin_center, emp_vario, nugget=False) + # allow 10 percent relative error + self.assertLess(_rel_err(mod.var, self.cmod.var), 0.1) + self.assertLess(_rel_err(mod.len_scale, self.cmod.len_scale), 0.1) + + def test_krige(self): + bin_max = np.deg2rad(8) + bin_edges = np.linspace(0, bin_max, 5) + emp_vario = gs.vario_estimate( + (self.data[:, 0], self.data[:, 1]), + self.data[:, 2], + bin_edges, + latlon=True, + ) + mod = gs.Spherical(latlon=True, geo_scale=gs.KM_SCALE) + mod.fit_variogram(*emp_vario, nugget=False) + kri = gs.krige.Ordinary( + mod, + (self.data[:, 0], self.data[:, 1]), + self.data[:, 2], + ) + field, var = kri((self.data[:, 0], self.data[:, 1])) + for i, dat in enumerate(self.data[:, 2]): + self.assertAlmostEqual(field[i], dat) + + def test_cond_srf(self): + bin_max = np.deg2rad(8) + bin_edges = np.linspace(0, bin_max, 5) + emp_vario = gs.vario_estimate( + (self.data[:, 0], self.data[:, 1]), + self.data[:, 2], + bin_edges, + latlon=True, + ) + mod = gs.Spherical(latlon=True, geo_scale=gs.KM_SCALE) + mod.fit_variogram(*emp_vario, nugget=False) + krige = gs.krige.Ordinary( + mod, (self.data[:, 0], self.data[:, 1]), self.data[:, 2] + ) + crf = gs.CondSRF(krige) + field = crf((self.data[:, 0], self.data[:, 1])) + for i, dat in enumerate(self.data[:, 2]): + self.assertAlmostEqual(field[i], dat, 3) + + def test_error(self): + # try fitting directional variogram + mod = gs.Gaussian(latlon=True) + with self.assertRaises(ValueError): + mod.fit_variogram([0, 1], [[0, 1], [0, 1], [0, 1]]) + # try to use fixed dim=2 with latlon + with self.assertRaises(ValueError): + ErrMod(latlon=True) + # try to estimate latlon vario on wrong dim + with self.assertRaises(ValueError): + gs.vario_estimate([[1], [1], [1]], [1], [0, 1], latlon=True) + # try to estimate directional vario with latlon + with self.assertRaises(ValueError): + gs.vario_estimate([[1], [1]], [1], [0, 1], latlon=True, angles=1) + # try to create a vector field with latlon + with self.assertRaises(ValueError): + srf = gs.SRF(mod, generator="VectorField", mode_no=2) + srf([1, 2]) + + +if __name__ == "__main__": + unittest.main() diff --git a/GSTools/source/tests/test_normalizer.py b/GSTools/source/tests/test_normalizer.py new file mode 100644 index 0000000000000000000000000000000000000000..9ab0f45e06fd182fbb289679422c30e995a18684 --- /dev/null +++ b/GSTools/source/tests/test_normalizer.py @@ -0,0 +1,223 @@ +""" +This is the unittest of the Normalizer class. +""" + +import unittest + +import numpy as np + +import gstools as gs + + +def _rel_err(a, b): + return np.abs(a / ((a + b) / 2) - 1) + + +class TestNormalizer(unittest.TestCase): + def setUp(self): + self.seed = 20210111 + self.rng = gs.random.RNG(self.seed) + self.mean = 11.1 + self.std = 2.25 + self.smp = self.rng.random.normal(self.mean, self.std, 1000) + self.lmb = 1.5 + + def test_fitting(self): + # boxcox with given data to init + bc_samples = gs.normalizer.BoxCox(lmbda=self.lmb).denormalize(self.smp) + bc_norm = gs.normalizer.BoxCox(data=bc_samples) + self.assertLess(_rel_err(self.lmb, bc_norm.lmbda), 1e-2) + self.assertAlmostEqual( + bc_norm.likelihood(bc_samples), + np.exp(bc_norm.loglikelihood(bc_samples)), + ) + # yeo-johnson with calling fit + yj_norm = gs.normalizer.YeoJohnson(lmbda=self.lmb) + yj_samples = yj_norm.denormalize(self.smp) + yj_norm.fit(yj_samples) + self.assertLess(_rel_err(self.lmb, yj_norm.lmbda), 1e-2) + self.assertAlmostEqual( + yj_norm.likelihood(yj_samples), + np.exp(yj_norm.loglikelihood(yj_samples)), + ) + # modulus with calling fit + mo_norm = gs.normalizer.Modulus(lmbda=self.lmb) + mo_samples = mo_norm.denormalize(self.smp) + mo_norm.fit(mo_samples) + self.assertLess(_rel_err(self.lmb, mo_norm.lmbda), 1e-2) + self.assertAlmostEqual( + mo_norm.likelihood(mo_samples), + np.exp(mo_norm.loglikelihood(mo_samples)), + ) + # manly with calling fit + ma_norm = gs.normalizer.Manly(lmbda=self.lmb) + ma_samples = ma_norm.denormalize(self.smp) + ma_norm.fit(ma_samples) + self.assertLess(_rel_err(self.lmb, ma_norm.lmbda), 1e-2) + # self.assertAlmostEqual( + # ma_norm.likelihood(ma_samples), + # np.exp(ma_norm.loglikelihood(ma_samples)), + # ) # this is comparing infs + + def test_boxcox(self): + # without shift + bc = gs.normalizer.BoxCox(lmbda=0) + self.assertTrue( + np.all( + np.isclose(self.smp, bc.normalize(bc.denormalize(self.smp))) + ) + ) + bc.lmbda = self.lmb + self.assertTrue( + np.all( + np.isclose(self.smp, bc.normalize(bc.denormalize(self.smp))) + ) + ) + # with shift + bc = gs.normalizer.BoxCoxShift(lmbda=0, shift=1.1) + self.assertTrue( + np.all( + np.isclose(self.smp, bc.normalize(bc.denormalize(self.smp))) + ) + ) + bc.lmbda = self.lmb + self.assertTrue( + np.all( + np.isclose(self.smp, bc.normalize(bc.denormalize(self.smp))) + ) + ) + + def test_yeojohnson(self): + yj = gs.normalizer.YeoJohnson(lmbda=0) + self.assertTrue( + np.all( + np.isclose( + self.smp - self.mean, + yj.normalize(yj.denormalize(self.smp - self.mean)), + ) + ) + ) + yj.lmbda = 2 + self.assertTrue( + np.all( + np.isclose( + self.smp - self.mean, + yj.normalize(yj.denormalize(self.smp - self.mean)), + ) + ) + ) + # with shift + yj.lmbda = self.lmb + self.assertTrue( + np.all( + np.isclose( + self.smp - self.mean, + yj.normalize(yj.denormalize(self.smp - self.mean)), + ) + ) + ) + + def test_modulus(self): + mo = gs.normalizer.Modulus(lmbda=0) + self.assertTrue( + np.all( + np.isclose(self.smp, mo.normalize(mo.denormalize(self.smp))) + ) + ) + mo.lmbda = self.lmb + self.assertTrue( + np.all( + np.isclose(self.smp, mo.normalize(mo.denormalize(self.smp))) + ) + ) + + def test_manly(self): + ma = gs.normalizer.Manly(lmbda=0) + self.assertTrue( + np.all( + np.isclose(self.smp, ma.normalize(ma.denormalize(self.smp))) + ) + ) + ma.lmbda = self.lmb + self.assertTrue( + np.all( + np.isclose(self.smp, ma.normalize(ma.denormalize(self.smp))) + ) + ) + + def test_parameterless(self): + no = gs.normalizer.LogNormal() + self.assertTrue( + np.all( + np.isclose(self.smp, no.normalize(no.denormalize(self.smp))) + ) + ) + no = gs.normalizer.Normalizer() + self.assertTrue( + np.all( + np.isclose(self.smp, no.normalize(no.denormalize(self.smp))) + ) + ) + + def test_compare(self): + norm1 = gs.normalizer.BoxCox() + norm2 = gs.normalizer.BoxCox(lmbda=0.5) + norm3 = gs.normalizer.YeoJohnson() + norm4 = "this is not a normalizer" + # check campare + self.assertTrue(norm1 == norm1) + self.assertTrue(norm1 != norm2) + self.assertTrue(norm1 != norm3) + self.assertTrue(norm1 != norm4) + + def test_check(self): + self.assertRaises(ValueError, gs.field.Field, gs.Cubic(), normalizer=5) + + def test_auto_fit(self): + x = y = range(60) + pos = gs.generate_grid([x, y]) + model = gs.Gaussian(dim=2, var=1, len_scale=10) + srf = gs.SRF( + model, seed=20170519, normalizer=gs.normalizer.LogNormal() + ) + srf(pos) + ids = np.arange(srf.field.size) + samples = np.random.RandomState(20210201).choice( + ids, size=60, replace=False + ) + # sample conditioning points from generated field + cond_pos = pos[:, samples] + cond_val = srf.field[samples] + krige = gs.krige.Ordinary( + model=gs.Stable(dim=2), + cond_pos=cond_pos, + cond_val=cond_val, + normalizer=gs.normalizer.BoxCox(), + fit_normalizer=True, + fit_variogram=True, + ) + # test fitting during kriging + self.assertTrue(np.abs(krige.normalizer.lmbda - 0.0) < 1e-1) + self.assertAlmostEqual(krige.model.len_scale, 10.2677, places=4) + self.assertAlmostEqual( + krige.model.sill, + krige.normalizer.normalize(cond_val).var(), + places=4, + ) + # test fitting during vario estimate + bin_center, gamma, normalizer = gs.vario_estimate( + cond_pos, + cond_val, + normalizer=gs.normalizer.BoxCox, + fit_normalizer=True, + ) + model = gs.Stable(dim=2) + model.fit_variogram(bin_center, gamma) + self.assertAlmostEqual(model.var, 0.6426670183, places=4) + self.assertAlmostEqual(model.len_scale, 9.635193952, places=4) + self.assertAlmostEqual(model.nugget, 0.001617908408, places=4) + self.assertAlmostEqual(model.alpha, 2.0, places=4) + + +if __name__ == "__main__": + unittest.main() diff --git a/GSTools/source/tests/test_pgs.py b/GSTools/source/tests/test_pgs.py new file mode 100644 index 0000000000000000000000000000000000000000..dd07406bc6550489f9b69c43497fdeef9e8430dc --- /dev/null +++ b/GSTools/source/tests/test_pgs.py @@ -0,0 +1,221 @@ +""" +This is the unittest of the PGS class. +""" + +import unittest + +import numpy as np + +import gstools as gs + + +class TestPGS(unittest.TestCase): + def test_struct_1d(self): + n = 100 + x = np.arange(n) + model = gs.Gaussian(dim=1, var=2, len_scale=15) + srf = gs.SRF(model, seed=436239) + field = srf.structured((x,)) + + m = 10 + lithotypes = np.zeros(n) + lithotypes[n // 3 - m // 2 : n // 3 + m // 2] = 1 + lithotypes[n // 3 - 2 * m : n // 3 - m // 2] = 2 + lithotypes[4 * n // 5 - m // 2 : 4 * n // 5 + m // 2] = 3 + + pgs = gs.PGS(1, field) + P = pgs(lithotypes) + self.assertAlmostEqual(P[n // 2], 0.0) + self.assertAlmostEqual(P[0], 0.0) + self.assertAlmostEqual(P[-1], 1.0) + self.assertAlmostEqual(P[-20], 3.0) + + def test_struct_2d(self): + n1 = 100 + n2 = 100 + pos = [np.arange(n1), np.arange(n2)] + + model1 = gs.Gaussian(dim=2, var=1, len_scale=10) + srf1 = gs.SRF(model1, seed=20170519) + field1 = srf1.structured(pos) + + model2 = gs.Gaussian(dim=2, var=5, len_scale=20) + srf2 = gs.SRF(model2, seed=20160519) + field2 = srf2.structured(pos) + + # create rectangle in middle of L + m1 = 16 + m2 = 16 + + lithotypes = np.zeros((n1, n2)) + lithotypes[ + n1 // 2 - m1 // 2 : n1 // 2 + m1 // 2, + n2 // 2 - m2 // 2 : n2 // 2 + m2 // 2, + ] = 1 + lithotypes[ + n1 // 2 - m1 // 2 + m1 : n1 // 2 + m1 // 2 + m1, + n2 // 2 - m2 // 2 : n2 // 2 + m2 // 2, + ] = 2 + lithotypes[ + n1 // 2 - m1 // 2 + m1 : n1 // 2 + m1 // 2 + m1, + n2 // 2 - m2 // 2 + m2 : n2 // 2 + m2 // 2 + m2, + ] = 3 + lithotypes[ + n1 // 3 - m1 // 2 : n1 // 3 + m1 // 2, + n2 // 3 - m2 // 2 : n2 // 3 + m2 // 2, + ] = 4 + + pgs = gs.PGS(2, [field1, field2]) + P = pgs(lithotypes) + + self.assertAlmostEqual(P[n1 // 2, n2 // 2], 2.0) + self.assertAlmostEqual(P[0, 0], 1.0) + self.assertAlmostEqual(P[-1, -1], 1.0) + self.assertAlmostEqual(P[0, -1], 0.0) + self.assertAlmostEqual(P[-1, 0], 1.0) + + def test_struct_3d(self): + n1 = 30 + n2 = 30 + n3 = 30 + pos = [np.arange(n1), np.arange(n2), np.arange(n3)] + + model1 = gs.Gaussian(dim=3, var=1, len_scale=10) + srf1 = gs.SRF(model1, seed=20170519) + field1 = srf1.structured(pos) + + model2 = gs.Gaussian(dim=3, var=5, len_scale=20) + srf2 = gs.SRF(model2, seed=20160519) + field2 = srf2.structured(pos) + + model3 = gs.Gaussian(dim=3, var=0.1, len_scale=5) + srf3 = gs.SRF(model3, seed=20191219) + field3 = srf3.structured(pos) + + # create rectangle in middle of L + m1 = 10 + m2 = 10 + m3 = 10 + + lithotypes = np.zeros((n1, n2, n3)) + lithotypes[ + n1 // 2 - m1 // 2 : n1 // 2 + m1 // 2, + n2 // 2 - m2 // 2 : n2 // 2 + m2 // 2, + n3 // 2 - m3 // 2 : n3 // 2 + m3 // 2, + ] = 1 + + pgs = gs.PGS(3, [field1, field2, field3]) + P = pgs(lithotypes) + + self.assertAlmostEqual(P[n1 // 2, n2 // 2, n3 // 2], 1.0) + self.assertAlmostEqual(P[n1 // 3, n2 // 3, n3 // 3], 1.0) + self.assertAlmostEqual(P[2 * n1 // 3, 2 * n2 // 3, 2 * n3 // 3], 1.0) + self.assertAlmostEqual(P[0, 0, 0], 1.0) + self.assertAlmostEqual(P[-1, -1, -1], 0.0) + self.assertAlmostEqual(P[-1, 0, 0], 1.0) + self.assertAlmostEqual(P[0, -1, 0], 1.0) + self.assertAlmostEqual(P[0, 0, -1], 1.0) + self.assertAlmostEqual(P[0, -1, -1], 1.0) + self.assertAlmostEqual(P[-1, 0, -1], 0.0) + self.assertAlmostEqual(P[-1, -1, 0], 1.0) + + def test_struct_4d(self): + n1 = 20 + n2 = 20 + n3 = 20 + n4 = 20 + pos = [np.arange(n1), np.arange(n2), np.arange(n4), np.arange(n4)] + + model1 = gs.Gaussian(dim=4, var=1, len_scale=10) + srf1 = gs.SRF(model1, seed=20170519) + field1 = srf1.structured(pos) + + model2 = gs.Gaussian(dim=4, var=5, len_scale=20) + srf2 = gs.SRF(model2, seed=20160519) + field2 = srf2.structured(pos) + + model3 = gs.Gaussian(dim=4, var=0.1, len_scale=5) + srf3 = gs.SRF(model3, seed=20191219) + field3 = srf3.structured(pos) + + model4 = gs.Exponential(dim=4, var=0.5, len_scale=12) + srf3 = gs.SRF(model4, seed=20191219) + field4 = srf3.structured(pos) + + # create rectangle in middle of L + m1 = 5 + m2 = 5 + m3 = 5 + m4 = 5 + + lithotypes = np.zeros((n1, n2, n3, n4)) + lithotypes[ + n1 // 2 - m1 // 2 : n1 // 2 + m1 // 2, + n2 // 2 - m2 // 2 : n2 // 2 + m2 // 2, + n3 // 2 - m3 // 2 : n3 // 2 + m3 // 2, + n4 // 2 - m4 // 2 : n4 // 2 + m4 // 2, + ] = 1 + + pgs = gs.PGS(4, [field1, field2, field3, field4]) + P = pgs(lithotypes) + + self.assertAlmostEqual(P[n1 // 2, n2 // 2, n3 // 2, n4 // 2], 1.0) + self.assertAlmostEqual(P[0, 0, 0, 0], 0.0) + self.assertAlmostEqual(P[-1, -1, -1, -1], 0.0) + + def test_unstruct_2d(self): + n1 = 10 + n2 = 8 + rng = np.random.RandomState(seed=438430) + x_unstruct = rng.randint(0, n1, size=n1 * n2) + y_unstruct = rng.randint(0, n2, size=n1 * n2) + + pos_unstruct = [x_unstruct, y_unstruct] + + model1 = gs.Gaussian(dim=2, var=1, len_scale=10) + srf1 = gs.SRF(model1, seed=20170519) + field1_unstruct = srf1.unstructured(pos_unstruct) + + model2 = gs.Gaussian(dim=2, var=5, len_scale=20) + srf2 = gs.SRF(model2, seed=20160519) + field2_unstruct = srf2.unstructured(pos_unstruct) + + # create rectangle in middle of L + m1 = 4 + m2 = 4 + + lithotypes_struct = np.zeros((n1, n2)) + lithotypes_struct[ + n1 // 2 - m1 // 2 : n1 // 2 + m1 // 2, + n2 // 2 - m2 // 2 : n2 // 2 + m2 // 2, + ] = 1 + lithotypes_struct[ + n1 // 2 - m1 // 2 + m1 : n1 // 2 + m1 // 2 + m1, + n2 // 2 - m2 // 2 : n2 // 2 + m2 // 2, + ] = 2 + lithotypes_struct[ + n1 // 2 - m1 // 2 + m1 : n1 // 2 + m1 // 2 + m1, + n2 // 2 - m2 // 2 + m2 : n2 // 2 + m2 // 2 + m2, + ] = 3 + lithotypes_struct[ + n1 // 3 - m1 // 2 : n1 // 3 + m1 // 2, + n2 // 3 - m2 // 2 : n2 // 3 + m2 // 2, + ] = 4 + + pgs = gs.PGS(2, [field1_unstruct, field2_unstruct]) + P = pgs(lithotypes_struct) + + self.assertAlmostEqual(P[0], 4.0) + self.assertAlmostEqual(P[-1], 1.0) + self.assertAlmostEqual(P[n1 * n2 // 2], 1.0) + + def test_assertions(self): + n = 30 + pos = [np.arange(n), np.arange(n), np.arange(n)] + lithotypes_2d = np.empty((n, n)) + field1 = np.empty((n, n)) + field2 = np.empty((n - 1, n - 1)) + pgs = gs.PGS(3, pos) + self.assertRaises(ValueError, gs.PGS, 3, [0, 1]) + self.assertRaises(ValueError, pgs, lithotypes_2d) + self.assertRaises(ValueError, gs.PGS, 2, [field1, field2]) diff --git a/GSTools/source/tests/test_randmeth.py b/GSTools/source/tests/test_randmeth.py new file mode 100644 index 0000000000000000000000000000000000000000..7cbb962fb9f9958974470fcb96174648409499dd --- /dev/null +++ b/GSTools/source/tests/test_randmeth.py @@ -0,0 +1,75 @@ +""" +This is the unittest of the RandMeth class. +""" + +import copy +import unittest + +import numpy as np + +from gstools import Gaussian +from gstools.field.generator import RandMeth + + +class TestRandMeth(unittest.TestCase): + def setUp(self): + self.cov_model_1d = Gaussian(dim=1, var=1.5, len_scale=3.5) + self.cov_model_2d = copy.deepcopy(self.cov_model_1d) + self.cov_model_2d.dim = 2 + self.cov_model_3d = copy.deepcopy(self.cov_model_1d) + self.cov_model_3d.dim = 3 + self.seed = 19031977 + self.x_grid = np.linspace(0.0, 10.0, 9) + self.y_grid = np.linspace(-5.0, 5.0, 16) + self.z_grid = np.linspace(-6.0, 7.0, 8) + self.x_tuple = np.linspace(0.0, 10.0, 10) + self.y_tuple = np.linspace(-5.0, 5.0, 10) + self.z_tuple = np.linspace(-6.0, 8.0, 10) + + self.rm_1d = RandMeth(self.cov_model_1d, mode_no=100, seed=self.seed) + self.rm_2d = RandMeth(self.cov_model_2d, mode_no=100, seed=self.seed) + self.rm_3d = RandMeth(self.cov_model_3d, mode_no=100, seed=self.seed) + + def test_unstruct_1d(self): + modes = self.rm_1d((self.x_tuple,)) + self.assertAlmostEqual(modes[0], 3.19799030) + self.assertAlmostEqual(modes[1], 2.44848295) + + def test_unstruct_2d(self): + modes = self.rm_2d((self.x_tuple, self.y_tuple)) + self.assertAlmostEqual(modes[0], 1.67318010) + self.assertAlmostEqual(modes[1], 2.12310269) + + def test_unstruct_3d(self): + modes = self.rm_3d((self.x_tuple, self.y_tuple, self.z_tuple)) + self.assertAlmostEqual(modes[0], 1.3240234883187239) + self.assertAlmostEqual(modes[1], 1.6367244277732766) + + def test_reset(self): + modes = self.rm_2d((self.x_tuple, self.y_tuple)) + self.assertAlmostEqual(modes[0], 1.67318010) + self.assertAlmostEqual(modes[1], 2.12310269) + + self.rm_2d.seed = self.rm_2d.seed + modes = self.rm_2d((self.x_tuple, self.y_tuple)) + self.assertAlmostEqual(modes[0], 1.67318010) + self.assertAlmostEqual(modes[1], 2.12310269) + + self.rm_2d.seed = 74893621 + modes = self.rm_2d((self.x_tuple, self.y_tuple)) + self.assertAlmostEqual(modes[0], -1.94278053) + self.assertAlmostEqual(modes[1], -1.12401651) + + self.rm_1d.model = self.cov_model_3d + modes = self.rm_1d((self.x_tuple, self.y_tuple, self.z_tuple)) + self.assertAlmostEqual(modes[0], 1.3240234883187239) + self.assertAlmostEqual(modes[1], 1.6367244277732766) + + self.rm_2d.mode_no = 800 + modes = self.rm_2d((self.x_tuple, self.y_tuple)) + self.assertAlmostEqual(modes[0], -3.20809251) + self.assertAlmostEqual(modes[1], -2.62032778) + + +if __name__ == "__main__": + unittest.main() diff --git a/GSTools/source/tests/test_rng.py b/GSTools/source/tests/test_rng.py new file mode 100644 index 0000000000000000000000000000000000000000..26cadb855662b8b1f94808c6da50fa74dffd49b7 --- /dev/null +++ b/GSTools/source/tests/test_rng.py @@ -0,0 +1,116 @@ +""" +This is the unittest of the RNG class. +""" + +import unittest + +import numpy as np + +from gstools import Gaussian +from gstools.random.rng import RNG + + +class TestRNG(unittest.TestCase): + def setUp(self): + self.seed = 19031977 + self.rng = RNG(self.seed) + self.many_modes = 1000000 + self.few_modes = 100 + + def test_rng_normal_consistency(self): + rng = RNG(21021997) + z1_refs = [-1.93013270, 0.46330478] + z2_refs = [-0.25536086, 0.98298696] + + z1 = self.rng.random.normal(size=self.few_modes) + z2 = self.rng.random.normal(size=self.few_modes) + self.assertAlmostEqual(z1[0], z1_refs[0]) + self.assertAlmostEqual(z1[1], z1_refs[1]) + self.assertAlmostEqual(z2[0], z2_refs[0]) + self.assertAlmostEqual(z2[1], z2_refs[1]) + self.rng.seed = self.seed + z1 = self.rng.random.normal(size=self.few_modes) + z2 = self.rng.random.normal(size=self.few_modes) + self.assertAlmostEqual(z1[0], z1_refs[0]) + self.assertAlmostEqual(z1[1], z1_refs[1]) + self.assertAlmostEqual(z2[0], z2_refs[0]) + self.assertAlmostEqual(z2[1], z2_refs[1]) + + def test_sample_sphere_1d(self): + dim = 1 + sphere_coord = self.rng.sample_sphere(dim, self.few_modes) + self.assertEqual(sphere_coord.shape, (dim, self.few_modes)) + sphere_coord = self.rng.sample_sphere(dim, self.many_modes) + self.assertAlmostEqual(np.mean(sphere_coord), 0.0, places=3) + + def test_sample_sphere_2d(self): + dim = 2 + sphere_coord = self.rng.sample_sphere(dim, self.few_modes) + np.testing.assert_allclose( + np.ones(self.few_modes), + sphere_coord[0, :] ** 2 + sphere_coord[1, :] ** 2, + ) + sphere_coord = self.rng.sample_sphere(dim, self.many_modes) + self.assertAlmostEqual(np.mean(sphere_coord), 0.0, places=3) + + def test_sample_sphere_3d(self): + dim = 3 + sphere_coord = self.rng.sample_sphere(dim, self.few_modes) + self.assertEqual(sphere_coord.shape, (dim, self.few_modes)) + np.testing.assert_allclose( + np.ones(self.few_modes), + sphere_coord[0, :] ** 2 + + sphere_coord[1, :] ** 2 + + sphere_coord[2, :] ** 2, + ) + sphere_coord = self.rng.sample_sphere(dim, self.many_modes) + self.assertAlmostEqual(np.mean(sphere_coord), 0.0, places=3) + + def test_sample_dist(self): + model = Gaussian(dim=1, var=3.5, len_scale=8.0) + pdf, cdf, ppf = model.dist_func + rad = self.rng.sample_dist( + size=self.few_modes, pdf=pdf, cdf=cdf, ppf=ppf, a=0 + ) + self.assertEqual(rad.shape[0], self.few_modes) + + model = Gaussian(dim=2, var=3.5, len_scale=8.0) + pdf, cdf, ppf = model.dist_func + rad = self.rng.sample_dist( + size=self.few_modes, pdf=pdf, cdf=cdf, ppf=ppf, a=0 + ) + self.assertEqual(rad.shape[0], self.few_modes) + + model = Gaussian(dim=3, var=3.5, len_scale=8.0) + pdf, cdf, ppf = model.dist_func + rad = self.rng.sample_dist( + size=self.few_modes, pdf=pdf, cdf=cdf, ppf=ppf, a=0 + ) + self.assertEqual(rad.shape[0], self.few_modes) + + # model = Gaussian(dim=2, var=3.5, len_scale=8.) + # pdf, cdf, ppf = model.dist_func + # rad = self.rng.sample_dist( + # size=self.many_modes, pdf=pdf, cdf=cdf, ppf=ppf, a=0) + # import matplotlib.pyplot as pt + # pt.hist(rad, bins=30) + # print(rad) + # pt.show() + + # TODO test with different models + + # TODO rework this, needs + # from scipy.stats import kurtosis, normaltest, skew + # def test_gau(self): + # for d in range(len(self.rngs)): + # Z, k = self.rngs[d]('gau', self.len_scale, self.many_modes) + # self.assertEqual(k.shape, (d+1, self.many_modes)) + # self.assertAlmostEqual(np.mean(k), 0., places=2) + # self.assertAlmostEqual(np.std(k), 1/self.len_scale, places=2) + # self.assertAlmostEqual(skew(k[0, :]), 0., places=2) + # self.assertAlmostEqual(kurtosis(k[0, :]), 0., places=1) + # self.assertLess(normaltest(k[0, :])[1], 0.05) + + +if __name__ == "__main__": + unittest.main() diff --git a/GSTools/source/tests/test_srf.py b/GSTools/source/tests/test_srf.py new file mode 100644 index 0000000000000000000000000000000000000000..8226a0461d96c2e1b2c7fac43f966d72d58d18ee --- /dev/null +++ b/GSTools/source/tests/test_srf.py @@ -0,0 +1,333 @@ +#!/usr/bin/env python +""" +This is the unittest of SRF class. +""" + +import unittest + +import meshio +import numpy as np + +import gstools as gs + +HAS_PYVISTA = False +try: + import pyvista as pv + + HAS_PYVISTA = True +except ImportError: + pass + + +class TestSRF(unittest.TestCase): + def setUp(self): + self.cov_model = gs.Gaussian(dim=2, var=1.5, len_scale=4.0) + self.mean = 0.3 + self.mode_no = 100 + + self.seed = 825718662 + self.x_grid = np.linspace(0.0, 12.0, 48) + self.y_grid = np.linspace(0.0, 10.0, 46) + self.z_grid = np.linspace(0.0, 10.0, 40) + + self.x_grid_c = np.linspace(-6.0, 6.0, 8) + self.y_grid_c = np.linspace(-6.0, 6.0, 8) + self.z_grid_c = np.linspace(-6.0, 6.0, 8) + + rng = np.random.RandomState(123018) + self.x_tuple = rng.uniform(0.0, 10, 100) + self.y_tuple = rng.uniform(0.0, 10, 100) + self.z_tuple = rng.uniform(0.0, 10, 100) + + def test_shape_1d(self): + self.cov_model.dim = 1 + srf = gs.SRF(self.cov_model, mean=self.mean, mode_no=self.mode_no) + field_str = srf([self.x_grid], seed=self.seed, mesh_type="structured") + field_unstr = srf( + [self.x_tuple], seed=self.seed, mesh_type="unstructured" + ) + self.assertEqual(field_str.shape, (len(self.x_grid),)) + self.assertEqual(field_unstr.shape, (len(self.x_tuple),)) + + def test_shape_2d(self): + self.cov_model.dim = 2 + srf = gs.SRF(self.cov_model, mean=self.mean, mode_no=self.mode_no) + field_str = srf( + (self.x_grid, self.y_grid), seed=self.seed, mesh_type="structured" + ) + field_unstr = srf( + (self.x_tuple, self.y_tuple), + seed=self.seed, + mesh_type="unstructured", + ) + self.assertEqual(field_str.shape, (len(self.x_grid), len(self.y_grid))) + self.assertEqual(field_unstr.shape, (len(self.x_tuple),)) + + def test_shape_3d(self): + self.cov_model.dim = 3 + srf = gs.SRF(self.cov_model, mean=self.mean, mode_no=self.mode_no) + field_str = srf( + (self.x_grid, self.y_grid, self.z_grid), + seed=self.seed, + mesh_type="structured", + ) + field_unstr = srf( + (self.x_tuple, self.y_tuple, self.z_tuple), + seed=987654, + mesh_type="unstructured", + ) + self.assertEqual( + field_str.shape, + (len(self.x_grid), len(self.y_grid), len(self.z_grid)), + ) + self.assertEqual(field_unstr.shape, (len(self.x_tuple),)) + + def test_anisotropy_2d(self): + self.cov_model.dim = 2 + srf = gs.SRF(self.cov_model, mean=self.mean, mode_no=self.mode_no) + field_iso = srf( + (self.x_grid, self.y_grid), seed=self.seed, mesh_type="structured" + ) + self.cov_model.anis = 0.5 + srf = gs.SRF(self.cov_model, mean=self.mean, mode_no=self.mode_no) + field_aniso = srf( + (self.x_grid, self.y_grid), seed=self.seed, mesh_type="structured" + ) + self.assertAlmostEqual(field_iso[0, 0], field_aniso[0, 0]) + self.assertAlmostEqual(field_iso[0, 4], field_aniso[0, 2]) + self.assertAlmostEqual(field_iso[0, 10], field_aniso[0, 5]) + + def test_anisotropy_3d(self): + self.cov_model.dim = 3 + srf = gs.SRF(self.cov_model, mean=self.mean, mode_no=self.mode_no) + field_iso = srf( + (self.x_grid, self.y_grid, self.z_grid), + seed=self.seed, + mesh_type="structured", + ) + self.cov_model.anis = (0.5, 4.0) + srf = gs.SRF(self.cov_model, mean=self.mean, mode_no=self.mode_no) + field_aniso = srf( + (self.x_grid, self.y_grid, self.z_grid), + seed=self.seed, + mesh_type="structured", + ) + self.assertAlmostEqual(field_iso[0, 0, 0], field_aniso[0, 0, 0]) + self.assertAlmostEqual(field_iso[0, 4, 0], field_aniso[0, 2, 0]) + self.assertAlmostEqual(field_iso[0, 10, 0], field_aniso[0, 5, 0]) + self.assertAlmostEqual(field_iso[0, 0, 0], field_aniso[0, 0, 0]) + self.assertAlmostEqual(field_iso[0, 0, 1], field_aniso[0, 0, 4]) + self.assertAlmostEqual(field_iso[0, 0, 3], field_aniso[0, 0, 12]) + + def test_rotation_unstruct_2d(self): + self.cov_model.dim = 2 + x_len = len(self.x_grid_c) + y_len = len(self.y_grid_c) + x_u, y_u = np.meshgrid(self.x_grid_c, self.y_grid_c) + x_u = np.reshape(x_u, x_len * y_len) + y_u = np.reshape(y_u, x_len * y_len) + + self.cov_model.anis = 0.25 + srf = gs.SRF(self.cov_model, mean=self.mean, mode_no=self.mode_no) + + field = srf((x_u, y_u), seed=self.seed, mesh_type="unstructured") + field_str = np.reshape(field, (y_len, x_len)) + + self.cov_model.angles = -np.pi / 2.0 + srf = gs.SRF(self.cov_model, mean=self.mean, mode_no=self.mode_no) + field_rot = srf((x_u, y_u), seed=self.seed, mesh_type="unstructured") + field_rot_str = np.reshape(field_rot, (y_len, x_len)) + + self.assertAlmostEqual(field_str[0, 0], field_rot_str[-1, 0]) + self.assertAlmostEqual(field_str[1, 2], field_rot_str[-3, 1]) + + def test_rotation_struct_2d(self): + self.cov_model.dim = 2 + self.cov_model.anis = 0.25 + srf = gs.SRF(self.cov_model, mean=self.mean, mode_no=self.mode_no) + field = srf( + (self.x_grid_c, self.y_grid_c), + seed=self.seed, + mesh_type="structured", + ) + + self.cov_model.angles = -np.pi / 2.0 + srf = gs.SRF(self.cov_model, mean=self.mean, mode_no=self.mode_no) + field_rot = srf( + (self.x_grid_c, self.y_grid_c), + seed=self.seed, + mesh_type="structured", + ) + + self.assertAlmostEqual(field[0, 0], field_rot[0, -1]) + self.assertAlmostEqual(field[1, 2], field_rot[2, 6]) + + def test_rotation_unstruct_3d(self): + self.cov_model = gs.Gaussian( + dim=3, var=1.5, len_scale=4.0, anis=(0.25, 0.5) + ) + x_len = len(self.x_grid_c) + y_len = len(self.y_grid_c) + z_len = len(self.z_grid_c) + x_u, y_u, z_u = np.meshgrid( + self.x_grid_c, self.y_grid_c, self.z_grid_c + ) + x_u = np.reshape(x_u, x_len * y_len * z_len) + y_u = np.reshape(y_u, x_len * y_len * z_len) + z_u = np.reshape(z_u, x_len * y_len * z_len) + + srf = gs.SRF(self.cov_model, mean=self.mean, mode_no=self.mode_no) + field = srf((x_u, y_u, z_u), seed=self.seed, mesh_type="unstructured") + field_str = np.reshape(field, (y_len, x_len, z_len)) + + self.cov_model.angles = (-np.pi / 2.0, -np.pi / 2.0) + srf = gs.SRF(self.cov_model, mean=self.mean, mode_no=self.mode_no) + field_rot = srf( + (x_u, y_u, z_u), seed=self.seed, mesh_type="unstructured" + ) + field_rot_str = np.reshape(field_rot, (y_len, x_len, z_len)) + + self.assertAlmostEqual(field_str[0, 0, 0], field_rot_str[-1, -1, 0]) + self.assertAlmostEqual(field_str[1, 2, 0], field_rot_str[-3, -1, 1]) + self.assertAlmostEqual(field_str[0, 0, 1], field_rot_str[-1, -2, 0]) + + def test_rotation_struct_3d(self): + self.cov_model.dim = 3 + self.cov_model.anis = 0.25 + srf = gs.SRF(self.cov_model, mean=self.mean, mode_no=self.mode_no) + field = srf( + (self.x_grid_c, self.y_grid_c, self.z_grid_c), + seed=self.seed, + mesh_type="structured", + ) + + self.cov_model.angles = -np.pi / 2.0 + srf = gs.SRF(self.cov_model, mean=self.mean, mode_no=self.mode_no) + field_rot = srf( + (self.x_grid_c, self.y_grid_c, self.z_grid_c), + seed=self.seed, + mesh_type="structured", + ) + + self.assertAlmostEqual(field[0, 0, 0], field_rot[0, 7, 0]) + self.assertAlmostEqual(field[0, 0, 1], field_rot[0, 7, 1]) + + self.cov_model.angles = (0, -np.pi / 2.0) + srf = gs.SRF(self.cov_model, mean=self.mean, mode_no=self.mode_no) + field_rot = srf( + (self.x_grid_c, self.y_grid_c, self.z_grid_c), + seed=self.seed, + mesh_type="structured", + ) + + self.assertAlmostEqual(field[0, 0, 0], field_rot[7, 0, 0]) + self.assertAlmostEqual(field[0, 1, 0], field_rot[7, 1, 0]) + self.assertAlmostEqual(field[1, 1, 0], field_rot[7, 1, 1]) + + def test_calls(self): + srf = gs.SRF(self.cov_model, mean=self.mean, mode_no=self.mode_no) + field = srf((self.x_tuple, self.y_tuple), seed=self.seed) + field2 = srf.unstructured((self.x_tuple, self.y_tuple), seed=self.seed) + self.assertAlmostEqual(field[0], srf.field[0]) + self.assertAlmostEqual(field[0], field2[0]) + field = srf( + (self.x_tuple, self.y_tuple), + seed=self.seed, + mesh_type="structured", + ) + field2 = srf.structured((self.x_tuple, self.y_tuple), seed=self.seed) + self.assertAlmostEqual(field[0, 0], srf.field[0, 0]) + self.assertAlmostEqual(field[0, 0], field2[0, 0]) + + @unittest.skipIf(not HAS_PYVISTA, "PyVista is not installed") + def test_mesh_pyvista(self): + """Test the `.mesh` call with various PyVista meshes.""" + # Create model + self.cov_model.dim = 3 + srf = gs.SRF(self.cov_model, mean=self.mean, mode_no=self.mode_no) + # Get the field the normal way for comparison + field = srf((self.x_tuple, self.y_tuple, self.z_tuple), seed=self.seed) + # Create mesh space with PyVista + pv_mesh = pv.PolyData(np.c_[self.x_tuple, self.y_tuple, self.z_tuple]) + # Run the helper + _ = srf.mesh(pv_mesh, seed=self.seed, points="centroids") + self.assertTrue(np.allclose(field, pv_mesh["field"])) + # points="centroids" + _ = srf.mesh(pv_mesh, seed=self.seed, points="points") + self.assertTrue(np.allclose(field, pv_mesh["field"])) + + def test_incomprrandmeth(self): + self.cov_model = gs.Gaussian(dim=2, var=0.5, len_scale=1.0) + srf = gs.SRF( + self.cov_model, + mean=self.mean, + mode_no=self.mode_no, + generator="IncomprRandMeth", + mean_velocity=0.5, + ) + field = srf((self.x_tuple, self.y_tuple), seed=476356) + self.assertAlmostEqual(field[0, 0], 1.23693272) + self.assertAlmostEqual(field[0, 1], 0.89242284) + field = srf( + (self.x_grid, self.y_grid), seed=4734654, mesh_type="structured" + ) + self.assertAlmostEqual(field[0, 0, 0], 1.07812013) + self.assertAlmostEqual(field[0, 1, 0], 1.06180674) + + # TODO put these checks into test_cov_model + def test_assertions(self): + # self.cov_model.dim = 0 + # self.assertRaises(ValueError, gs.SRF, self.cov_model, self.mean, self.mode_no) + # self.cov_model.dim = 4 + # self.assertRaises(ValueError, gs.SRF, self.cov_model, self.mean, self.mode_no) + self.cov_model.dim = 3 + self.cov_model.anis = (0.25, 0.5) + srf = gs.SRF(self.cov_model, mean=self.mean, mode_no=self.mode_no) + self.assertRaises(ValueError, srf, [self.x_tuple]) + self.assertRaises(ValueError, srf, [self.x_grid, self.y_grid]) + srf = gs.SRF(self.cov_model, mean=self.mean, mode_no=self.mode_no) + self.assertRaises(ValueError, srf, [self.x_tuple, self.y_tuple]) + self.assertRaises( + ValueError, srf, [self.x_grid, self.y_grid, self.z_grid] + ) + # everything not "unstructured" is treated as "structured" + # self.assertRaises( + # ValueError, + # srf, + # [self.x_tuple, self.y_tuple, self.z_tuple], + # self.seed, + # mesh_type="hyper_mesh", + # ) + + def test_meshio(self): + points = np.array( + [ + [0.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + [1.0, 0.0, 0.0], + ] + ) + cells = [("tetra", np.array([[0, 1, 2, 3]]))] + mesh = meshio.Mesh(points, cells) + model = gs.Gaussian(dim=3, len_scale=0.1) + srf = gs.SRF(model) + srf.mesh(mesh, points="points") + self.assertEqual(len(srf.field), 4) + srf.mesh(mesh, points="centroids") + self.assertEqual(len(srf.field), 1) + + def test_grid_generation(self): + pos1 = [self.x_grid, self.y_grid, self.z_grid] + pos2 = gs.generate_grid(pos1) + time = np.arange(10) + grid1 = gs.generate_grid(pos1 + [time]) + grid2 = gs.generate_st_grid(pos1, time, mesh_type="structured") + grid3 = gs.generate_st_grid(pos2, time, mesh_type="unstructured") + self.assertTrue(np.all(np.isclose(grid1, grid2))) + self.assertTrue(np.all(np.isclose(grid1, grid3))) + self.assertTrue(np.all(np.isclose(grid2, grid3))) + + +if __name__ == "__main__": + unittest.main() diff --git a/GSTools/source/tests/test_sum_model.py b/GSTools/source/tests/test_sum_model.py new file mode 100644 index 0000000000000000000000000000000000000000..a1bd181f195d2815018f716f907bf11cba554d4e --- /dev/null +++ b/GSTools/source/tests/test_sum_model.py @@ -0,0 +1,245 @@ +""" +This is the unittest of the SumModel class. +""" + +import unittest +from pathlib import Path + +import numpy as np + +import gstools as gs + + +class TestSumModel(unittest.TestCase): + def test_init(self): + s1 = gs.SumModel(dim=3) + s2 = gs.Nugget(dim=3) + self.assertTrue(s1 == s2) + + m1 = gs.Nugget(nugget=1) + s1 = m1 + m1 + m1 + s2 = gs.Nugget(nugget=3) + self.assertTrue(s1 == s2) + self.assertFalse(m1 == s2) + + # Nugget cant set positive var or len-scale + with self.assertRaises(ValueError): + m1.var = 1 + with self.assertRaises(ValueError): + m1.len_scale = 1 + + def test_attr_set(self): + s1 = gs.Gaussian() + gs.Gaussian() + gs.Gaussian() + with self.assertRaises(ValueError): + s1.vars = [1, 2] + with self.assertRaises(ValueError): + s1.len_scales = [1, 2] + + s1.integral_scale = 10 + self.assertAlmostEqual(s1.integral_scale_0, 10) + self.assertAlmostEqual(s1.integral_scale_1, 10) + self.assertAlmostEqual(s1.integral_scale_2, 10) + + s1.var = 2 + s1.ratios = [0.2, 0.2, 0.6] + self.assertAlmostEqual(s1.vars[0], 0.4) + self.assertAlmostEqual(s1.vars[1], 0.4) + self.assertAlmostEqual(s1.vars[2], 1.2) + + with self.assertRaises(ValueError): + s1.ratios = [0.3, 0.2, 0.6] + with self.assertRaises(ValueError): + s1.ratios = [0.3, 0.2] + + def test_compare(self): + s1 = gs.Gaussian(var=1) + gs.Exponential(var=2) + s2 = gs.Exponential(var=1) + gs.Gaussian(var=2) + self.assertFalse(s1 == gs.Nugget(dim=3)) + self.assertFalse(s1 == s2) + self.assertFalse(gs.Exponential() == (gs.Exponential() + gs.Nugget())) + self.assertFalse((gs.Exponential() + gs.Nugget()) == gs.Exponential()) + + def test_copy(self): + # check that models get copied + m1 = gs.Gaussian() + s1 = m1 + m1 + var = [1.0, 2.0] + s1.vars = var + np.testing.assert_array_almost_equal(s1.vars, var) + + def test_var_dist(self): + s1 = gs.SumModel(gs.Exponential, gs.Exponential, var=3) + np.testing.assert_array_almost_equal(s1.vars, [1.5, 1.5]) + + def test_presence(self): + s1 = gs.SumModel(gs.Exponential, gs.Exponential) + self.assertFalse(gs.Gaussian() in s1) + + def test_len_dist(self): + s1 = gs.SumModel(gs.Exponential, gs.Exponential, len_scale=10) + np.testing.assert_array_almost_equal(s1.len_scales, [10, 10]) + + def test_temporal(self): + s1 = gs.SumModel( + gs.Exponential, gs.Exponential, temporal=True, spatial_dim=2 + ) + self.assertTrue(all(mod.temporal for mod in s1)) + self.assertTrue(all(mod.dim == 3 for mod in s1)) + + def test_magic(self): + m1 = gs.Gaussian(dim=2, var=1.0, len_scale=1.0) + m2 = gs.Matern(dim=2, var=2.0, len_scale=2.0, nu=2.0) + m3 = gs.Integral(dim=2, var=3.0, len_scale=3.0, nu=3.0) + s1 = gs.SumModel(m1, m2, m3) + s2 = m1 + m2 + m3 + s3 = gs.SumModel( + *(gs.Gaussian, gs.Matern, gs.Integral), + dim=2, + vars=[1.0, 2.0, 3.0], + len_scales=[1.0, 2.0, 3.0], + nu_1=2.0, + nu_2=3.0, + ) + self.assertTrue(s1 == s2) + self.assertTrue(s2 == s3) + self.assertTrue(s1 == s3) + + def test_exceptions(self): + with self.assertRaises(ValueError): + gs.Exponential() + 1 + + with self.assertRaises(ValueError): + 1 + gs.Exponential() + + with self.assertRaises(ValueError): + gs.SumModel(gs.Exponential, gs.Gaussian(dim=2)) + + with self.assertRaises(ValueError): + gs.SumModel(gs.Nugget, gs.Nugget(nugget=2)) + + with self.assertRaises(ValueError): + gs.SumModel(gs.Gaussian(dim=2), gs.Exponential) + + with self.assertRaises(ValueError): + gs.SumModel(gs.Nugget(nugget=2), gs.Nugget) + + with self.assertRaises(ValueError): + model = gs.Spherical() + gs.Spherical() + model[0].dim = 2 + model.check() + + with self.assertRaises(ValueError): + model = gs.Spherical() + gs.Spherical() + model[0].nugget = 2 + model.check() + + with self.assertRaises(ValueError): + gs.Spherical(latlon=True) + gs.Spherical() + + with self.assertRaises(ValueError): + gs.Spherical(geo_scale=2) + gs.Spherical() + + with self.assertRaises(ValueError): + gs.Spherical(temporal=True) + gs.Spherical() + + with self.assertRaises(ValueError): + gs.Spherical(anis=0.5) + gs.Spherical() + + with self.assertRaises(ValueError): + gs.Spherical(angles=0.5) + gs.Spherical() + + def test_generate(self): + x = np.random.RandomState(19970221).rand(1000) * 100.0 + y = np.random.RandomState(20011012).rand(1000) * 100.0 + m1 = gs.Spherical(dim=2, var=2, len_scale=5) + m2 = gs.Spherical(dim=2, var=1, len_scale=10) + m3 = gs.Gaussian(dim=2, var=1, len_scale=20) + model = m1 + m2 + m3 + srf = gs.SRF(model, mean=0, seed=199702212) + field = srf((x, y)) + self.assertAlmostEqual(np.var(field), 3.7, places=1) + # used for test_fit (see below) + # bin_center, gamma = gs.vario_estimate((x, y), field, max_dist=50, bin_no=100) + + def test_fit(self): + here = Path(__file__).parent + bin_center, gamma = np.loadtxt(here / "data" / "variogram.txt") + s2 = gs.SumModel(gs.Gaussian, gs.Spherical, gs.Spherical, dim=2) + res1, _ = s2.fit_variogram(bin_center, gamma, nugget=False) + res2, _ = s2.fit_variogram( + bin_center, gamma, nugget=False, len_scale_2=5, var_0=1 + ) + res3, _ = s2.fit_variogram(bin_center, gamma, nugget=False, var=3.7) + res4, _ = s2.fit_variogram( + bin_center, gamma, nugget=False, len_scale=15 + ) + res5, _ = s2.fit_variogram( + bin_center, gamma, len_scale=15, sill=3.7, var_1=1 + ) + res6, _ = s2.fit_variogram( + bin_center, gamma, len_scale=15, sill=3.7, var_1=1, nugget=0.1 + ) + res7, _ = s2.fit_variogram( + bin_center, + gamma, + len_scale=15, + sill=3.7, + var_1=1, + var_2=1, + nugget=0.1, + ) + + self.assertAlmostEqual(res1["var"], 3.7, places=1) + self.assertAlmostEqual(res2["var"], 3.7, places=1) + self.assertAlmostEqual(res3["var"], 3.7, places=5) + self.assertAlmostEqual(res4["len_scale"], 15.0, places=5) + self.assertAlmostEqual(res5["var"] + res5["nugget"], 3.7, places=2) + self.assertAlmostEqual(res5["len_scale"], 15.0, places=5) + self.assertAlmostEqual(res6["var"] + res6["nugget"], 3.7, places=2) + self.assertAlmostEqual(res7["var_0"], 1.6, places=5) + + mod_n = gs.Nugget(dim=2) + res, _ = mod_n.fit_variogram(bin_center, gamma) + self.assertAlmostEqual(res["nugget"], 3.4, places=1) + # nothing to fit + with self.assertRaises(ValueError): + mod_n.fit_variogram(bin_center, gamma, nugget=False) + # fixed sub-vars greated than fixed total var + with self.assertRaises(ValueError): + s2.fit_variogram(bin_center, gamma, var=1, var_0=1, var_1=1) + # fixed len_scale and sub-len-scale not possible + with self.assertRaises(ValueError): + s2.fit_variogram(bin_center, gamma, len_scale=15, len_scale_0=5) + + def test_sum_weights(self): + mod = gs.Gaussian() + gs.Gaussian() + gs.Gaussian() + # fixed sub-vars too big + with self.assertRaises(ValueError): + mod.set_var_weights([1], skip=[1, 2], var=1) + # too many ids + with self.assertRaises(ValueError): + mod.set_var_weights([1, 1], skip=[1, 2]) + # wrong skip + with self.assertRaises(ValueError): + mod.set_var_weights([1, 1], skip=[10]) + + mod = gs.Gaussian() + gs.Gaussian() + gs.Gaussian() + # fixed sub-lens too big + with self.assertRaises(ValueError): + mod.set_len_weights([1], skip=[1, 2], len_scale=0.1) + # too many ids + with self.assertRaises(ValueError): + mod.set_len_weights([1, 1], skip=[1, 2]) + # wrong skip + with self.assertRaises(ValueError): + mod.set_len_weights([1, 1], skip=[10]) + # check setting with skipping + mod.set_len_weights([1, 1], skip=[0], len_scale=10) + np.testing.assert_array_almost_equal(mod.len_scales, [1, 14.5, 14.5]) + # check setting + mod.set_len_weights([1, 1, 2], len_scale=10) + np.testing.assert_array_almost_equal(mod.len_scales, [7.5, 7.5, 15]) + + +if __name__ == "__main__": + unittest.main() diff --git a/GSTools/source/tests/test_temporal.py b/GSTools/source/tests/test_temporal.py new file mode 100644 index 0000000000000000000000000000000000000000..c179db1c2322071c41b75410f349e21eaa7563f7 --- /dev/null +++ b/GSTools/source/tests/test_temporal.py @@ -0,0 +1,78 @@ +""" +This is the unittest for temporal related routines. +""" + +import unittest + +import numpy as np + +import gstools as gs + + +class TestTemporal(unittest.TestCase): + def setUp(self): + self.mod = gs.Gaussian( + latlon=True, + temporal=True, + len_scale=1000, + anis=0.5, + geo_scale=gs.KM_SCALE, + ) + + def test_latlon(self): + mod = gs.Gaussian( + latlon=True, temporal=True, angles=[1, 2, 3, 4, 5, 6] + ) + self.assertEqual(mod.dim, 4) + self.assertEqual(mod.field_dim, 3) + self.assertEqual(mod.spatial_dim, 2) + self.assertTrue(np.allclose(mod.angles, 0)) + + mod1 = gs.Gaussian(latlon=True, temporal=True, len_scale=[10, 5]) + mod2 = gs.Gaussian(latlon=True, temporal=True, len_scale=10, anis=0.5) + + self.assertTrue(np.allclose(mod1.anis, mod2.anis)) + self.assertAlmostEqual(mod1.len_scale, mod2.len_scale) + + def test_latlon2pos(self): + self.assertAlmostEqual( + 8, self.mod.anisometrize(self.mod.isometrize((8, 6, 9)))[0, 0] + ) + self.assertAlmostEqual( + 6, self.mod.anisometrize(self.mod.isometrize((8, 6, 9)))[1, 0] + ) + self.assertAlmostEqual( + 9, self.mod.anisometrize(self.mod.isometrize((8, 6, 9)))[2, 0] + ) + self.assertAlmostEqual( + gs.EARTH_RADIUS, + self.mod.isometrize( + self.mod.anisometrize((gs.EARTH_RADIUS, 0, 0, 10)) + )[0, 0], + ) + self.assertAlmostEqual( + 10, + self.mod.isometrize( + self.mod.anisometrize((gs.EARTH_RADIUS, 0, 0, 10)) + )[3, 0], + ) + + def test_rotation(self): + mod = gs.Gaussian( + spatial_dim=3, temporal=True, angles=[1, 2, 3, 4, 5, 6] + ) + self.assertTrue(np.allclose(mod.angles, [1, 2, 3, 0, 0, 0])) + self.assertEqual(mod.dim, 4) + + def test_krige(self): + # auto-fitting latlon-temporal model in kriging not possible + with self.assertRaises(ValueError): + kri = gs.Krige(self.mod, 3 * [[1, 2]], [1, 2], fit_variogram=True) + + def test_field(self): + srf = gs.SRF(self.mod) + self.assertTrue(srf.temporal) + + +if __name__ == "__main__": + unittest.main() diff --git a/GSTools/source/tests/test_transform.py b/GSTools/source/tests/test_transform.py new file mode 100644 index 0000000000000000000000000000000000000000..abc5505b09113ed4fcc2a8eac2b7b2e4abd567a4 --- /dev/null +++ b/GSTools/source/tests/test_transform.py @@ -0,0 +1,189 @@ +"""This is the unittest of the transform submodule.""" + +import unittest + +import numpy as np + +import gstools as gs + + +class TestTransform(unittest.TestCase): + def setUp(self): + self.cov_model = gs.Gaussian(dim=2, var=1.5, len_scale=4.0) + self.mean = 0.3 + self.mode_no = 100 + + self.seed = 825718662 + self.x_grid = np.linspace(0.0, 12.0, 48) + self.y_grid = np.linspace(0.0, 10.0, 46) + + self.methods = [ + "binary", + "boxcox", + "zinnharvey", + "normal_force_moments", + "normal_to_lognormal", + "normal_to_uniform", + "normal_to_arcsin", + "normal_to_uquad", + ] + + def test_transform_normal(self): + srf = gs.SRF(self.cov_model, mean=self.mean, mode_no=self.mode_no) + srf((self.x_grid, self.y_grid), seed=self.seed, mesh_type="structured") + for method in self.methods: + srf.transform(method, store=method) + std = np.sqrt(srf.model.var) + self.assertTrue(set(self.methods) == set(srf.field_names[1:])) + # force moments + self.assertAlmostEqual(srf["normal_force_moments"].mean(), srf.mean) + self.assertAlmostEqual(srf["normal_force_moments"].var(), std**2) + # binary + np.testing.assert_allclose( + np.unique(srf.binary), srf.mean + np.array([-std, std]) + ) + # boxcox + np.testing.assert_allclose( + srf.field, gs.normalizer.BoxCox().normalize(srf.boxcox) + ) + with self.assertWarns(Warning): + srf.transform("boxcox", store="boxcox_warn", lmbda=2) + # lognormal + np.testing.assert_allclose(srf.field, np.log(srf.normal_to_lognormal)) + srf.transform("boxcox", store="boxcox2", lmbda=0) + np.testing.assert_allclose(srf.boxcox2, srf.normal_to_lognormal) + # unifrom + self.assertTrue(np.all(srf.normal_to_uniform < 1)) + self.assertTrue(np.all(srf.normal_to_uniform > 0)) + # how to test arcsin and uquad?! + + # discrete + values = [-1, 0, 1] + thresholds = [-0.9, 0.1] + srf.transform( + "discrete", values=values, thresholds=thresholds, store="f1" + ) + np.testing.assert_allclose(np.unique(srf.f1), [-1, 0, 1]) + + values = [-1, 0, 1] + srf.transform( + "discrete", values=values, thresholds="arithmetic", store="f2" + ) + np.testing.assert_allclose(np.unique(srf.f2), [-1.0, 0.0, 1.0]) + + values = [-1, 0, 0.5, 1] + srf.transform( + "discrete", values=values, thresholds="equal", store="f3" + ) + np.testing.assert_allclose(np.unique(srf.f3), values) + # checks + with self.assertRaises(ValueError): + srf.transform("discrete", values=values, thresholds=[1]) + with self.assertRaises(ValueError): + srf.transform("discrete", values=values, thresholds=[1, 3, 2]) + + # function + srf.transform("function", function=lambda x: 2 * x, store="f4") + np.testing.assert_allclose(2 * srf.field, srf.f4) + with self.assertRaises(ValueError): + srf.transform("function", function=None) + + # unknown method + with self.assertRaises(ValueError): + srf.transform("foobar") + + def test_transform_denormal(self): + srf_fail = gs.SRF( + model=self.cov_model, + mean=self.mean, + mode_no=self.mode_no, + trend=lambda x, y: x, + ) + srf_fail((self.x_grid, self.y_grid), mesh_type="structured") + with self.assertRaises(ValueError): + srf_fail.transform("zinnharvey") + + srf_fail = gs.SRF( + model=self.cov_model, + mean=lambda x, y: x, + mode_no=self.mode_no, + ) + srf_fail((self.x_grid, self.y_grid), mesh_type="structured") + with self.assertRaises(ValueError): + srf_fail.transform("zinnharvey") + + srf = gs.SRF( + model=self.cov_model, + mean=self.mean, + mode_no=self.mode_no, + normalizer=gs.normalizer.LogNormal, + ) + srf((self.x_grid, self.y_grid), seed=self.seed, mesh_type="structured") + + for method in self.methods: + if method in ("normal_to_lognormal", "boxcox"): + continue + with self.assertRaises(ValueError): + srf.transform(method, store=method) + + for method in self.methods: + srf.transform(method, store=method, process=True) + std = np.sqrt(srf.model.var) + self.assertTrue(set(self.methods) == set(srf.field_names[1:])) + # force moments + self.assertAlmostEqual( + np.log(srf["normal_force_moments"]).mean(), srf.mean + ) + self.assertAlmostEqual( + np.log(srf["normal_force_moments"]).var(), std**2 + ) + # binary + np.testing.assert_allclose( + np.unique(np.log(srf.binary)), srf.mean + np.array([-std, std]) + ) + # boxcox + np.testing.assert_allclose( + np.log(srf.field), + gs.normalizer.BoxCox().normalize(np.log(srf.boxcox)), + ) + # lognormal + np.testing.assert_allclose(srf.field, np.log(srf.normal_to_lognormal)) + # unifrom + self.assertTrue(np.all(np.log(srf.normal_to_uniform) < 1)) + self.assertTrue(np.all(np.log(srf.normal_to_uniform) > 0)) + + # discrete + values = [-1, 0, 1] + thresholds = [-0.9, 0.1] + srf.transform( + "discrete", + values=values, + thresholds=thresholds, + store="f1", + process=True, + ) + np.testing.assert_allclose(np.unique(np.log(srf.f1)), [-1, 0, 1]) + + values = [-1, 0, 1] + srf.transform( + "discrete", + values=values, + thresholds="arithmetic", + store="f2", + process=True, + ) + np.testing.assert_allclose(np.unique(np.log(srf.f2)), [-1.0, 0.0, 1.0]) + + values = [-1, 0, 0.5, 1] + srf.transform( + "discrete", + values=values, + thresholds="equal", + store="f3", + process=True, + ) + np.testing.assert_allclose(np.unique(np.log(srf.f3)), values) + + +if __name__ == "__main__": + unittest.main() diff --git a/GSTools/source/tests/test_variogram_structured.py b/GSTools/source/tests/test_variogram_structured.py new file mode 100644 index 0000000000000000000000000000000000000000..e0a3525aa82d4d1910205c6c2baee67f640afe1d --- /dev/null +++ b/GSTools/source/tests/test_variogram_structured.py @@ -0,0 +1,278 @@ +""" +This is a unittest of the variogram module. +""" + +import unittest + +import numpy as np + +import gstools as gs + + +class TestVariogramStructured(unittest.TestCase): + def setUp(self): + pass + + def test_doubles(self): + z = np.array( + (41.2, 40.2, 39.7, 39.2, 40.1, 38.3, 39.1, 40.0, 41.1, 40.3), + dtype=np.double, + ) + gamma = gs.vario_estimate_axis(z) + self.assertAlmostEqual(gamma[1], 0.4917, places=4) + + def test_ints(self): + z = np.array((10, 20, 30, 40), dtype=int) + gamma = gs.vario_estimate_axis(z) + self.assertAlmostEqual(gamma[1], 50.0, places=4) + + def test_mixed(self): + z = np.array( + (41.2, 40.2, 39.7, 39.2, 40.1, 38.3, 39.1, 40.0, 41.1, 40.3), + dtype=np.double, + ) + gamma = gs.vario_estimate_axis(z) + self.assertAlmostEqual(gamma[1], 0.4917, places=4) + + z = np.array((10, 20, 30, 40), dtype=int) + + gamma = gs.vario_estimate_axis(z) + self.assertAlmostEqual(gamma[1], 50.0, places=4) + + def test_list(self): + z = [41.2, 40.2, 39.7, 39.2, 40.1, 38.3, 39.1, 40.0, 41.1, 40.3] + gamma = gs.vario_estimate_axis(z) + self.assertAlmostEqual(gamma[1], 0.4917, places=4) + + def test_cressie_1d(self): + z = [41.2, 40.2, 39.7, 39.2, 40.1, 38.3, 39.1, 40.0, 41.1, 40.3] + gamma = gs.vario_estimate_axis(z, estimator="cressie") + self.assertAlmostEqual(gamma[1], 1.546 / 2.0, places=3) + + def test_1d(self): + # literature values + z = np.array( + (41.2, 40.2, 39.7, 39.2, 40.1, 38.3, 39.1, 40.0, 41.1, 40.3), + dtype=np.double, + ) + gamma = gs.vario_estimate_axis(z) + self.assertAlmostEqual(gamma[0], 0.0000, places=4) + self.assertAlmostEqual(gamma[1], 0.4917, places=4) + self.assertAlmostEqual(gamma[2], 0.7625, places=4) + + def test_masked_1d(self): + # literature values + z = np.array( + (41.2, 40.2, 39.7, 39.2, 40.1, 38.3, 39.1, 40.0, 41.1, 40.3), + dtype=np.double, + ) + z_ma = np.ma.masked_array(z, mask=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + gamma = gs.vario_estimate_axis(z_ma) + self.assertAlmostEqual(gamma[0], 0.0000, places=4) + self.assertAlmostEqual(gamma[1], 0.4917, places=4) + self.assertAlmostEqual(gamma[2], 0.7625, places=4) + z_ma = np.ma.masked_array(z, mask=[1, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + gamma = gs.vario_estimate_axis(z_ma) + self.assertAlmostEqual(gamma[0], 0.0000, places=4) + self.assertAlmostEqual(gamma[1], 0.4906, places=4) + self.assertAlmostEqual(gamma[2], 0.7107, places=4) + + def test_masked_2d(self): + rng = np.random.RandomState(1479373475) + field = rng.rand(80, 60) + mask = np.zeros_like(field) + field_ma = np.ma.masked_array(field, mask=mask) + + gamma_x = gs.vario_estimate_axis(field_ma, direction="x") + gamma_y = gs.vario_estimate_axis(field_ma, direction="y") + + var = 1.0 / 12.0 + self.assertAlmostEqual(gamma_x[0], 0.0, places=2) + self.assertAlmostEqual(gamma_x[len(gamma_x) // 2], var, places=2) + self.assertAlmostEqual(gamma_x[-1], var, places=2) + self.assertAlmostEqual(gamma_y[0], 0.0, places=2) + self.assertAlmostEqual(gamma_y[len(gamma_y) // 2], var, places=2) + self.assertAlmostEqual(gamma_y[-1], var, places=2) + + mask = np.zeros_like(field) + mask[0, 0] = 1 + field = np.ma.masked_array(field, mask=mask) + gamma_x = gs.vario_estimate_axis(field_ma, direction="x") + gamma_y = gs.vario_estimate_axis(field_ma, direction="y") + self.assertAlmostEqual(gamma_x[0], 0.0, places=2) + self.assertAlmostEqual(gamma_y[0], 0.0, places=2) + + def test_masked_3d(self): + rng = np.random.RandomState(1479373475) + field = rng.rand(30, 30, 30) + mask = np.zeros_like(field) + field_ma = np.ma.masked_array(field, mask=mask) + + gamma_x = gs.vario_estimate_axis(field_ma, direction="x") + gamma_y = gs.vario_estimate_axis(field_ma, direction="y") + gamma_z = gs.vario_estimate_axis(field_ma, direction="z") + + var = 1.0 / 12.0 + self.assertAlmostEqual(gamma_x[0], 0.0, places=2) + self.assertAlmostEqual(gamma_x[len(gamma_x) // 2], var, places=2) + self.assertAlmostEqual(gamma_x[-1], var, places=2) + self.assertAlmostEqual(gamma_y[0], 0.0, places=2) + self.assertAlmostEqual(gamma_y[len(gamma_y) // 2], var, places=2) + self.assertAlmostEqual(gamma_y[-1], var, places=2) + self.assertAlmostEqual(gamma_z[0], 0.0, places=2) + self.assertAlmostEqual(gamma_z[len(gamma_y) // 2], var, places=2) + self.assertAlmostEqual(gamma_z[-1], var, places=2) + + mask = np.zeros_like(field) + mask[0, 0, 0] = 1 + field = np.ma.masked_array(field, mask=mask) + gamma_x = gs.vario_estimate_axis(field_ma, direction="x") + gamma_y = gs.vario_estimate_axis(field_ma, direction="y") + gamma_z = gs.vario_estimate_axis(field_ma, direction="z") + self.assertAlmostEqual(gamma_x[0], 0.0, places=2) + self.assertAlmostEqual(gamma_y[0], 0.0, places=2) + self.assertAlmostEqual(gamma_z[0], 0.0, places=2) + + def test_uncorrelated_2d(self): + x = np.linspace(0.0, 100.0, 80) + y = np.linspace(0.0, 100.0, 60) + + rng = np.random.RandomState(1479373475) + field = rng.rand(len(x), len(y)) + + gamma_x = gs.vario_estimate_axis(field, direction="x") + gamma_y = gs.vario_estimate_axis(field, direction="y") + + var = 1.0 / 12.0 + self.assertAlmostEqual(gamma_x[0], 0.0, places=2) + self.assertAlmostEqual(gamma_x[len(gamma_x) // 2], var, places=2) + self.assertAlmostEqual(gamma_x[-1], var, places=2) + self.assertAlmostEqual(gamma_y[0], 0.0, places=2) + self.assertAlmostEqual(gamma_y[len(gamma_y) // 2], var, places=2) + self.assertAlmostEqual(gamma_y[-1], var, places=2) + + def test_uncorrelated_cressie_2d(self): + x = np.linspace(0.0, 100.0, 80) + y = np.linspace(0.0, 100.0, 60) + + rng = np.random.RandomState(1479373475) + field = rng.rand(len(x), len(y)) + + gamma_x = gs.vario_estimate_axis( + field, direction="x", estimator="cressie" + ) + gamma_y = gs.vario_estimate_axis( + field, direction="y", estimator="cressie" + ) + + var = 1.0 / 12.0 + self.assertAlmostEqual(gamma_x[0], 0.0, places=1) + self.assertAlmostEqual(gamma_x[len(gamma_x) // 2], var, places=1) + self.assertAlmostEqual(gamma_y[0], 0.0, places=1) + self.assertAlmostEqual(gamma_y[len(gamma_y) // 2], var, places=1) + + def test_uncorrelated_3d(self): + x = np.linspace(0.0, 100.0, 30) + y = np.linspace(0.0, 100.0, 30) + z = np.linspace(0.0, 100.0, 30) + + rng = np.random.RandomState(1479373475) + field = rng.rand(len(x), len(y), len(z)) + + gamma = gs.vario_estimate_axis(field, "x") + gamma = gs.vario_estimate_axis(field, "y") + gamma = gs.vario_estimate_axis(field, "z") + + var = 1.0 / 12.0 + self.assertAlmostEqual(gamma[0], 0.0, places=2) + self.assertAlmostEqual(gamma[len(gamma) // 2], var, places=2) + self.assertAlmostEqual(gamma[-1], var, places=2) + + def test_directions_2d(self): + x = np.linspace(0.0, 20.0, 100) + y = np.linspace(0.0, 15.0, 80) + rng = np.random.RandomState(1479373475) + x_rand = rng.rand(len(x)) + y_rand = rng.rand(len(y)) + # random values repeated along y-axis + field_x = np.tile(x_rand, (len(y), 1)).T + # random values repeated along x-axis + field_y = np.tile(y_rand, (len(x), 1)) + + # gamma_x_x = gs.vario_estimate_axis(field_x, direction="x") + gamma_x_y = gs.vario_estimate_axis(field_x, direction="y") + + gamma_y_x = gs.vario_estimate_axis(field_y, direction="x") + # gamma_y_y = gs.vario_estimate_axis(field_y, direction="y") + + self.assertAlmostEqual(gamma_x_y[1], 0.0) + self.assertAlmostEqual(gamma_x_y[len(gamma_x_y) // 2], 0.0) + self.assertAlmostEqual(gamma_x_y[-1], 0.0) + self.assertAlmostEqual(gamma_y_x[1], 0.0) + self.assertAlmostEqual(gamma_y_x[len(gamma_x_y) // 2], 0.0) + self.assertAlmostEqual(gamma_y_x[-1], 0.0) + + def test_directions_3d(self): + x = np.linspace(0.0, 10.0, 20) + y = np.linspace(0.0, 15.0, 25) + z = np.linspace(0.0, 20.0, 30) + rng = np.random.RandomState(1479373475) + x_rand = rng.rand(len(x)) + y_rand = rng.rand(len(y)) + z_rand = rng.rand(len(z)) + + field_x = np.tile(x_rand.reshape((len(x), 1, 1)), (1, len(y), len(z))) + field_y = np.tile(y_rand.reshape((1, len(y), 1)), (len(x), 1, len(z))) + field_z = np.tile(z_rand.reshape((1, 1, len(z))), (len(x), len(y), 1)) + + # gamma_x_x = gs.vario_estimate_axis(field_x, direction="x") + gamma_x_y = gs.vario_estimate_axis(field_x, direction="y") + gamma_x_z = gs.vario_estimate_axis(field_x, direction="z") + + gamma_y_x = gs.vario_estimate_axis(field_y, direction="x") + # gamma_y_y = gs.vario_estimate_axis(field_y, direction="y") + gamma_y_z = gs.vario_estimate_axis(field_y, direction="z") + + gamma_z_x = gs.vario_estimate_axis(field_z, direction="x") + gamma_z_y = gs.vario_estimate_axis(field_z, direction="y") + # gamma_z_z = gs.vario_estimate_axis(field_z, direction="z") + + self.assertAlmostEqual(gamma_x_y[1], 0.0) + self.assertAlmostEqual(gamma_x_y[len(gamma_x_y) // 2], 0.0) + self.assertAlmostEqual(gamma_x_y[-1], 0.0) + self.assertAlmostEqual(gamma_x_z[1], 0.0) + self.assertAlmostEqual(gamma_x_z[len(gamma_x_y) // 2], 0.0) + self.assertAlmostEqual(gamma_x_z[-1], 0.0) + self.assertAlmostEqual(gamma_y_x[1], 0.0) + self.assertAlmostEqual(gamma_y_x[len(gamma_x_y) // 2], 0.0) + self.assertAlmostEqual(gamma_y_x[-1], 0.0) + self.assertAlmostEqual(gamma_y_z[1], 0.0) + self.assertAlmostEqual(gamma_y_z[len(gamma_x_y) // 2], 0.0) + self.assertAlmostEqual(gamma_y_z[-1], 0.0) + self.assertAlmostEqual(gamma_z_x[1], 0.0) + self.assertAlmostEqual(gamma_z_x[len(gamma_x_y) // 2], 0.0) + self.assertAlmostEqual(gamma_z_x[-1], 0.0) + self.assertAlmostEqual(gamma_z_y[1], 0.0) + self.assertAlmostEqual(gamma_z_y[len(gamma_x_y) // 2], 0.0) + self.assertAlmostEqual(gamma_z_y[-1], 0.0) + + def test_exceptions(self): + x = np.linspace(0.0, 10.0, 20) + # rng = np.random.RandomState(1479373475) + # x_rand = rng.rand(len(x)) + self.assertRaises(ValueError, gs.vario_estimate_axis, x, "a") + + def test_missing(self): + x = np.linspace(0.0, 10.0, 10) + x_nan = x.copy() + x_nan[0] = np.nan + x_mask = np.isnan(x_nan) + x = np.ma.array(x, mask=x_mask) + v1 = gs.vario_estimate_axis(x_nan) + v2 = gs.vario_estimate_axis(x) + for i in range(len(v1)): + self.assertAlmostEqual(v1[i], v2[i]) + + +if __name__ == "__main__": + unittest.main() diff --git a/GSTools/source/tests/test_variogram_unstructured.py b/GSTools/source/tests/test_variogram_unstructured.py new file mode 100644 index 0000000000000000000000000000000000000000..10b085bf00d7c2909d8a4c03048e5928264766a7 --- /dev/null +++ b/GSTools/source/tests/test_variogram_unstructured.py @@ -0,0 +1,409 @@ +""" +This is a unittest of the variogram module. +""" + +import unittest + +import numpy as np + +import gstools as gs + + +class TestVariogramUnstructured(unittest.TestCase): + def setUp(self): + model = gs.Exponential(dim=3, len_scale=[12, 6, 3]) + x = y = z = range(10) + self.pos = (x, y, z) + srf = gs.SRF(model, seed=123456) + self.field = srf((x, y, z), mesh_type="structured") + + def test_doubles(self): + x = np.arange(1, 11, 1, dtype=np.double) + z = np.array( + (41.2, 40.2, 39.7, 39.2, 40.1, 38.3, 39.1, 40.0, 41.1, 40.3), + dtype=np.double, + ) + bins = np.arange(1, 11, 1, dtype=np.double) + bin_centres, gamma = gs.vario_estimate([x], z, bins) + self.assertAlmostEqual(gamma[0], 0.4917, places=4) + + def test_ints(self): + x = np.arange(1, 5, 1, dtype=int) + z = np.array((10, 20, 30, 40), dtype=int) + bins = np.arange(1, 11, 1, dtype=int) + bin_centres, gamma = gs.vario_estimate([x], z, bins) + self.assertAlmostEqual(gamma[0], 50.0, places=4) + + def test_mixed(self): + x = np.arange(1, 11, 1, dtype=np.double) + z = np.array( + (41.2, 40.2, 39.7, 39.2, 40.1, 38.3, 39.1, 40.0, 41.1, 40.3), + dtype=np.double, + ) + bins = np.arange(1, 11, 1, dtype=int) + bin_centres, gamma = gs.vario_estimate([x], z, bins) + self.assertAlmostEqual(gamma[0], 0.4917, places=4) + + x = np.arange(1, 5, 1, dtype=np.double) + z = np.array((10, 20, 30, 40), dtype=int) + bins = np.arange(1, 11, 1, dtype=int) + bin_centres, gamma = gs.vario_estimate([x], z, bins) + self.assertAlmostEqual(gamma[0], 50.0, places=4) + + x = np.arange(1, 5, 1, dtype=np.double) + z = np.array((10, 20, 30, 40), dtype=int) + bins = np.arange(1, 11, 1, dtype=np.double) + bin_centres, gamma = gs.vario_estimate([x], z, bins) + self.assertAlmostEqual(gamma[0], 50.0, places=4) + + def test_list(self): + x = np.arange(1, 11, 1, dtype=np.double) + z = [41.2, 40.2, 39.7, 39.2, 40.1, 38.3, 39.1, 40.0, 41.1, 40.3] + bins = np.arange(1, 11, 1, dtype=np.double) + bin_centres, gamma = gs.vario_estimate([x], z, bins) + self.assertAlmostEqual(gamma[1], 0.7625, places=4) + + def test_1d(self): + x = np.arange(1, 11, 1, dtype=np.double) + # literature values + z = np.array( + (41.2, 40.2, 39.7, 39.2, 40.1, 38.3, 39.1, 40.0, 41.1, 40.3), + dtype=np.double, + ) + bins = np.arange(1, 11, 1, dtype=np.double) + bin_centres, gamma = gs.vario_estimate([x], z, bins) + self.assertAlmostEqual(gamma[0], 0.4917, places=4) + self.assertAlmostEqual(gamma[1], 0.7625, places=4) + + def test_uncorrelated_2d(self): + x_c = np.linspace(0.0, 100.0, 60) + y_c = np.linspace(0.0, 100.0, 60) + x, y = np.meshgrid(x_c, y_c) + x = np.reshape(x, len(x_c) * len(y_c)) + y = np.reshape(y, len(x_c) * len(y_c)) + + rng = np.random.RandomState(1479373475) + field = rng.rand(len(x)) + + bins = np.arange(0, 100, 10) + + bin_centres, gamma = gs.vario_estimate((x, y), field, bins) + + var = 1.0 / 12.0 + self.assertAlmostEqual(gamma[0], var, places=2) + self.assertAlmostEqual(gamma[len(gamma) // 2], var, places=2) + self.assertAlmostEqual(gamma[-1], var, places=2) + + def test_uncorrelated_3d(self): + x_c = np.linspace(0.0, 100.0, 15) + y_c = np.linspace(0.0, 100.0, 15) + z_c = np.linspace(0.0, 100.0, 15) + x, y, z = np.meshgrid(x_c, y_c, z_c) + x = np.reshape(x, len(x_c) * len(y_c) * len(z_c)) + y = np.reshape(y, len(x_c) * len(y_c) * len(z_c)) + z = np.reshape(z, len(x_c) * len(y_c) * len(z_c)) + + rng = np.random.RandomState(1479373475) + field = rng.rand(len(x)) + + bins = np.arange(0, 100, 10) + + bin_centres, gamma = gs.vario_estimate((x, y, z), field, bins) + + var = 1.0 / 12.0 + self.assertAlmostEqual(gamma[0], var, places=2) + self.assertAlmostEqual(gamma[len(gamma) // 2], var, places=2) + self.assertAlmostEqual(gamma[-1], var, places=2) + + def test_sampling_1d(self): + x = np.linspace(0.0, 100.0, 21000) + + rng = np.random.RandomState(1479373475) + field = rng.rand(len(x)) + + bins = np.arange(0, 100, 10) + + bin_centres, gamma = gs.vario_estimate( + [x], field, bins, sampling_size=5000, sampling_seed=1479373475 + ) + + var = 1.0 / 12.0 + self.assertAlmostEqual(gamma[0], var, places=2) + self.assertAlmostEqual(gamma[len(gamma) // 2], var, places=2) + self.assertAlmostEqual(gamma[-1], var, places=2) + + def test_sampling_2d(self): + x_c = np.linspace(0.0, 100.0, 600) + y_c = np.linspace(0.0, 100.0, 600) + x, y = np.meshgrid(x_c, y_c) + x = np.reshape(x, len(x_c) * len(y_c)) + y = np.reshape(y, len(x_c) * len(y_c)) + + rng = np.random.RandomState(1479373475) + field = rng.rand(len(x)) + + bins = np.arange(0, 100, 10) + + bin_centres, gamma = gs.vario_estimate( + (x, y), field, bins, sampling_size=2000, sampling_seed=1479373475 + ) + + var = 1.0 / 12.0 + self.assertAlmostEqual(gamma[0], var, places=2) + self.assertAlmostEqual(gamma[len(gamma) // 2], var, places=2) + self.assertAlmostEqual(gamma[-1], var, places=2) + + def test_sampling_3d(self): + x_c = np.linspace(0.0, 100.0, 100) + y_c = np.linspace(0.0, 100.0, 100) + z_c = np.linspace(0.0, 100.0, 100) + x, y, z = np.meshgrid(x_c, y_c, z_c) + x = np.reshape(x, len(x_c) * len(y_c) * len(z_c)) + y = np.reshape(y, len(x_c) * len(y_c) * len(z_c)) + z = np.reshape(z, len(x_c) * len(y_c) * len(z_c)) + + rng = np.random.RandomState(1479373475) + field = rng.rand(len(x)) + + bins = np.arange(0, 100, 10) + + bin_centres, gamma = gs.vario_estimate( + (x, y, z), + field, + bins, + sampling_size=2000, + sampling_seed=1479373475, + ) + var = 1.0 / 12.0 + self.assertAlmostEqual(gamma[0], var, places=2) + self.assertAlmostEqual(gamma[len(gamma) // 2], var, places=2) + self.assertAlmostEqual(gamma[-1], var, places=2) + + def test_assertions(self): + x = np.arange(0, 10) + x_e = np.arange(0, 11) + y = np.arange(0, 11) + y_e = np.arange(0, 12) + z = np.arange(0, 12) + z_e = np.arange(0, 15) + bins = np.arange(0, 3) + # bins_e = np.arange(0, 1) + field = np.arange(0, 10) + field_e = np.arange(0, 9) + + self.assertRaises(ValueError, gs.vario_estimate, [x_e], field, bins) + self.assertRaises(ValueError, gs.vario_estimate, (x, y_e), field, bins) + self.assertRaises( + ValueError, gs.vario_estimate, (x, y_e, z), field, bins + ) + self.assertRaises( + ValueError, gs.vario_estimate, (x, y, z_e), field, bins + ) + self.assertRaises( + ValueError, gs.vario_estimate, (x_e, y, z), field, bins + ) + self.assertRaises( + ValueError, gs.vario_estimate, (x, y, z), field_e, bins + ) + self.assertRaises(ValueError, gs.vario_estimate, [x], field_e, bins) + self.assertRaises( + ValueError, gs.vario_estimate, [x], field, bins, estimator="bla" + ) + + def test_multi_field(self): + x = np.random.RandomState(19970221).rand(100) * 100.0 + model = gs.Exponential(dim=1, var=2, len_scale=10) + srf = gs.SRF(model) + field1 = srf(x, seed=19970221) + field2 = srf(x, seed=20011012) + bins = np.arange(20) * 2 + bin_center, gamma1 = gs.vario_estimate(x, field1, bins) + bin_center, gamma2 = gs.vario_estimate(x, field2, bins) + bin_center, gamma = gs.vario_estimate(x, [field1, field2], bins) + gamma_mean = 0.5 * (gamma1 + gamma2) + for i in range(len(gamma)): + self.assertAlmostEqual(gamma[i], gamma_mean[i], places=2) + + def test_no_data(self): + x1 = np.random.RandomState(19970221).rand(100) * 100.0 + field1 = np.random.RandomState(20011012).rand(100) * 100.0 + field1[:10] = np.nan + x2 = x1[10:] + field2 = field1[10:] + bins = np.arange(20) * 2 + bin_center, gamma1 = gs.vario_estimate(x1, field1, bins) + bin_center, gamma2 = gs.vario_estimate(x2, field2, bins) + for i in range(len(gamma1)): + self.assertAlmostEqual(gamma1[i], gamma2[i], places=2) + + def test_direction_axis(self): + field = np.ma.array(self.field) + field.mask = np.abs(field) < 0.1 + bins = range(10) + __, vario_u = gs.vario_estimate( + *(self.pos, field, bins), + direction=((1, 0, 0), (0, 1, 0), (0, 0, 1)), # x-, y- and z-axis + bandwidth=0.25, # bandwith small enough to only match lines + mesh_type="structured", + ) + vario_s_x = gs.vario_estimate_axis(field, "x") + vario_s_y = gs.vario_estimate_axis(field, "y") + vario_s_z = gs.vario_estimate_axis(field, "z") + for i in range(len(bins) - 1): + self.assertAlmostEqual(vario_u[0][i], vario_s_x[i]) + self.assertAlmostEqual(vario_u[1][i], vario_s_y[i]) + self.assertAlmostEqual(vario_u[2][i], vario_s_z[i]) + + def test_direction_angle(self): + bins = range(0, 10, 2) + __, v2, c2 = gs.vario_estimate( + *(self.pos[:2], self.field[0], bins), + angles=np.pi / 4, # 45 deg + mesh_type="structured", + return_counts=True, + ) + __, v1, c1 = gs.vario_estimate( + *(self.pos[:2], self.field[0], bins), + direction=(1, 1), # 45 deg + mesh_type="structured", + return_counts=True, + ) + for i in range(len(bins) - 1): + self.assertAlmostEqual(v1[i], v2[i]) + self.assertEqual(c1[i], c2[i]) + + def test_direction_assertion(self): + pos = [[1, 2, 3], [1, 2, 3]] + bns = [1, 2] + fld = np.ma.array([1, 2, 3]) + self.assertRaises( # degenerated direction + ValueError, gs.vario_estimate, pos, fld, bns, direction=[0, 0] + ) + self.assertRaises( # wrong shape of direction + ValueError, gs.vario_estimate, pos, fld, bns, direction=[[[3, 1]]] + ) + self.assertRaises( # wrong dimension of direction + ValueError, gs.vario_estimate, pos, fld, bns, direction=[[3, 1, 2]] + ) + self.assertRaises( # wrong shape of angles + ValueError, gs.vario_estimate, pos, fld, bns, angles=[[[1]]] + ) + self.assertRaises( # wrong dimension of angles + ValueError, gs.vario_estimate, pos, fld, bns, angles=[[1, 1]] + ) + self.assertRaises( # direction on latlon + ValueError, + gs.vario_estimate, + pos, + fld, + bns, + direction=[1, 0], + latlon=True, + ) + + def test_mask_no_data(self): + pos = [[1, 2, 3, 4, 5], [1, 2, 3, 4, 5]] + bns = [0, 4] + fld1 = np.ma.array([1, 2, 3, 4, 5]) + fld2 = np.ma.array([np.nan, 2, 3, 4, 5]) + fld3 = np.ma.array([1, 2, 3, 4, 5]) + mask = [False, False, True, False, False] + fld1.mask = [True, False, False, False, False] + fld2.mask = mask + __, v1, c1 = gs.vario_estimate( + *(pos, fld1, bns), + mask=mask, + return_counts=True, + ) + __, v2, c2 = gs.vario_estimate(*(pos, fld2, bns), return_counts=True) + __, v3, c3 = gs.vario_estimate( + *(pos, fld3, bns), + no_data=1, + mask=mask, + return_counts=True, + ) + __, v4, c4 = gs.vario_estimate( + *(pos, fld3, bns), + mask=True, + return_counts=True, + ) + __, v5 = gs.vario_estimate(*(pos, fld3, bns), mask=True) + + self.assertAlmostEqual(v1[0], v2[0]) + self.assertAlmostEqual(v1[0], v3[0]) + self.assertEqual(c1[0], c2[0]) + self.assertEqual(c1[0], c3[0]) + self.assertAlmostEqual(v4[0], 0.0) + self.assertEqual(c4[0], 0) + self.assertAlmostEqual(v5[0], 0.0) + + def test_fit_directional(self): + model = gs.Stable(dim=3) + bins = [0, 3, 6, 9, 12] + model.len_scale_bounds = [0, 20] + bin_center, emp_vario, counts = gs.vario_estimate( + *(self.pos, self.field, bins), + direction=model.main_axes(), + mesh_type="structured", + return_counts=True, + ) + # check if this succeeds + model.fit_variogram(bin_center, emp_vario, sill=1, return_r2=True) + self.assertTrue(1 > model.anis[0] > model.anis[1]) + model.fit_variogram(bin_center, emp_vario, sill=1, anis=[0.5, 0.25]) + self.assertTrue(15 > model.len_scale) + model.fit_variogram(bin_center, emp_vario, sill=1, weights=counts) + len_save = model.len_scale + model.fit_variogram(bin_center, emp_vario, sill=1, weights=counts[0]) + self.assertAlmostEqual(len_save, model.len_scale) + # catch wrong dim for dir.-vario + with self.assertRaises(ValueError): + model.fit_variogram(bin_center, emp_vario[:2]) + + def test_auto_binning(self): + # structured mesh + bin_center, emp_vario = gs.vario_estimate( + self.pos, + self.field, + mesh_type="structured", + ) + self.assertEqual(len(bin_center), 21) + self.assertTrue(np.all(bin_center[1:] > bin_center[:-1])) + self.assertTrue(np.all(bin_center > 0)) + # unstructured mesh + bin_center, emp_vario = gs.vario_estimate( + self.pos, + self.field[:, 0, 0], + ) + self.assertEqual(len(bin_center), 8) + self.assertTrue(np.all(bin_center[1:] > bin_center[:-1])) + self.assertTrue(np.all(bin_center > 0)) + # latlon coords + bin_center, emp_vario = gs.vario_estimate( + self.pos[:2], + self.field[..., 0], + mesh_type="structured", + latlon=True, + ) + self.assertEqual(len(bin_center), 15) + self.assertTrue(np.all(bin_center[1:] > bin_center[:-1])) + self.assertTrue(np.all(bin_center > 0)) + + def test_standard_bins(self): + # structured mesh + bins = gs.standard_bins(self.pos, dim=3, mesh_type="structured") + self.assertEqual(len(bins), 22) + self.assertTrue(np.all(bins[1:] > bins[:-1])) + self.assertTrue(np.all(bins[1:] > 0)) + # no pos given + self.assertRaises(ValueError, gs.standard_bins) + + def test_raise(self): + # 1d field given for latlon estimation -> needs 2d + self.assertRaises( + ValueError, gs.vario_estimate, [[1, 2]], [1, 2], latlon=True + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/README.md b/README.md index 2a8cb1b5e7702319904447cef931ddf30773088b..fe5c99decaec90bd876409a6621c15921b69cad3 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,32 @@ --- -title: GSTools -emoji: 🐠 -colorFrom: indigo +title: Gstools MCP +emoji: 🤖 +colorFrom: blue colorTo: purple sdk: docker +sdk_version: "4.26.0" +app_file: app.py pinned: false --- -Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference +# Gstools MCP Service + +Auto-generated MCP service for GSTools. + +## Usage + +``` +https://None-GSTools-mcp.hf.space/mcp +``` + +## Connect with Cursor + +```json +{ + "mcpServers": { + "GSTools": { + "url": "https://None-GSTools-mcp.hf.space/mcp" + } + } +} +``` diff --git a/app.py b/app.py new file mode 100644 index 0000000000000000000000000000000000000000..d4d6ea6129aa00727bed92d0ff3a1c02ba27a113 --- /dev/null +++ b/app.py @@ -0,0 +1,45 @@ +from fastapi import FastAPI +import os +import sys + +mcp_plugin_path = os.path.join(os.path.dirname(__file__), "GSTools", "mcp_output", "mcp_plugin") +sys.path.insert(0, mcp_plugin_path) + +app = FastAPI( + title="Gstools MCP Service", + description="Auto-generated MCP service for GSTools", + version="1.0.0" +) + +@app.get("/") +def root(): + return { + "service": "Gstools MCP Service", + "version": "1.0.0", + "status": "running", + "transport": os.environ.get("MCP_TRANSPORT", "http") + } + +@app.get("/health") +def health_check(): + return {"status": "healthy", "service": "GSTools MCP"} + +@app.get("/tools") +def list_tools(): + try: + from mcp_service import create_app + mcp_app = create_app() + tools = [] + for tool_name, tool_func in mcp_app.tools.items(): + tools.append({ + "name": tool_name, + "description": tool_func.__doc__ or "No description available" + }) + return {"tools": tools} + except Exception as e: + return {"error": f"Failed to load tools: {str(e)}"} + +if __name__ == "__main__": + import uvicorn + port = int(os.environ.get("PORT", 7860)) + uvicorn.run(app, host="0.0.0.0", port=port) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..519b8e52a33c493e8ed136ec7455bce8ffe0754d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,12 @@ +fastmcp +fastapi +uvicorn[standard] +pydantic>=2.0.0 +gstools-cython>=1,<2 +emcee>=3.0.0 +hankel>=1.0.0 +meshio>=5.1.0 +numpy>=1.20.0 +pyevtk>=1.1.1 +scipy>=1.1.0 +matplotlib diff --git a/run_docker.ps1 b/run_docker.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..5ecb1bac99863a43b7b3531a71bf41d1819d1df4 --- /dev/null +++ b/run_docker.ps1 @@ -0,0 +1,35 @@ +cd $PSScriptRoot + +$ErrorActionPreference = "Stop" + +$entryName = if ($env:MCP_ENTRY_NAME) { $env:MCP_ENTRY_NAME } else { "GSTools" } +$entryUrl = if ($env:MCP_ENTRY_URL) { $env:MCP_ENTRY_URL } else { "http://localhost:7860/mcp" } +$imageName = if ($env:MCP_IMAGE_NAME) { $env:MCP_IMAGE_NAME } else { "GSTools-mcp" } + +$mcpDir = Join-Path $env:USERPROFILE ".cursor" +$mcpPath = Join-Path $mcpDir "mcp.json" +if (!(Test-Path $mcpDir)) { New-Item -ItemType Directory -Path $mcpDir | Out-Null } + +$config = @{} +if (Test-Path $mcpPath) { + try { $config = Get-Content $mcpPath -Raw | ConvertFrom-Json } catch { $config = @{} } +} + +# Rebuild mcpServers as ordered and append the entry last +$serversOrdered = [ordered]@{} +if ($config -and ($config.PSObject.Properties.Name -contains "mcpServers") -and $config.mcpServers) { + $existing = $config.mcpServers + if ($existing -is [pscustomobject]) { + foreach ($p in $existing.PSObject.Properties) { if ($p.Name -ne $entryName) { $serversOrdered[$p.Name] = $p.Value } } + } elseif ($existing -is [System.Collections.IDictionary]) { + foreach ($k in $existing.Keys) { if ($k -ne $entryName) { $serversOrdered[$k] = $existing[$k] } } + } +} +$serversOrdered[$entryName] = @{ url = $entryUrl } +$config = @{ mcpServers = $serversOrdered } + +$config | ConvertTo-Json -Depth 10 | Set-Content -Path $mcpPath -Encoding UTF8 +Write-Host ("Updated $entryName in " + $mcpPath + " -> " + $entryUrl) + +docker build -t $imageName . +docker run --rm -p 7860:7860 $imageName diff --git a/run_docker.sh b/run_docker.sh new file mode 100644 index 0000000000000000000000000000000000000000..05aeefb0dc67a07a152190c971ed33d553004476 --- /dev/null +++ b/run_docker.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Switch to the directory where this script is located +cd "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +mcp_entry_name="${MCP_ENTRY_NAME:-GSTools}" +mcp_entry_url="${MCP_ENTRY_URL:-http://localhost:7860/mcp}" +mcp_dir="${HOME}/.cursor" +mcp_path="${mcp_dir}/mcp.json" +mkdir -p "${mcp_dir}" + +if command -v python3 >/dev/null 2>&1; then +python3 - "${mcp_path}" "${mcp_entry_name}" "${mcp_entry_url}" <<'PY' +import json, os, sys +path, name, url = sys.argv[1:4] +cfg = {"mcpServers": {}} +if os.path.exists(path): + try: + with open(path, "r", encoding="utf-8") as f: + cfg = json.load(f) + except Exception: + cfg = {"mcpServers": {}} +if not isinstance(cfg, dict): + cfg = {"mcpServers": {}} +servers = cfg.get("mcpServers") +if not isinstance(servers, dict): + servers = {} +ordered = {} +for k, v in servers.items(): + if k != name: + ordered[k] = v +ordered[name] = {"url": url} +cfg = {"mcpServers": ordered} +with open(path, "w", encoding="utf-8") as f: + json.dump(cfg, f, indent=2, ensure_ascii=False) +PY +elif command -v python >/dev/null 2>&1; then +python - "${mcp_path}" "${mcp_entry_name}" "${mcp_entry_url}" <<'PY' +import json, os, sys +path, name, url = sys.argv[1:4] +cfg = {"mcpServers": {}} +if os.path.exists(path): + try: + with open(path, "r", encoding="utf-8") as f: + cfg = json.load(f) + except Exception: + cfg = {"mcpServers": {}} +if not isinstance(cfg, dict): + cfg = {"mcpServers": {}} +servers = cfg.get("mcpServers") +if not isinstance(servers, dict): + servers = {} +ordered = {} +for k, v in servers.items(): + if k != name: + ordered[k] = v +ordered[name] = {"url": url} +cfg = {"mcpServers": ordered} +with open(path, "w", encoding="utf-8") as f: + json.dump(cfg, f, indent=2, ensure_ascii=False) +PY +elif command -v jq >/dev/null 2>&1; then + name="${mcp_entry_name}"; url="${mcp_entry_url}" + if [ -f "${mcp_path}" ]; then + tmp="$(mktemp)" + jq --arg name "$name" --arg url "$url" ' + .mcpServers = (.mcpServers // {}) + | .mcpServers as $s + | ($s | with_entries(select(.key != $name))) as $base + | .mcpServers = ($base + {($name): {"url": $url}}) + ' "${mcp_path}" > "${tmp}" && mv "${tmp}" "${mcp_path}" + else + printf '{ "mcpServers": { "%s": { "url": "%s" } } } +' "$name" "$url" > "${mcp_path}" + fi +else + echo "Warning: neither python nor jq found; skipped updating ~/.cursor/mcp.json" >&2 +fi + +docker build -t GSTools-mcp . +docker run --rm -p 7860:7860 GSTools-mcp