diff --git a/.gitignore b/.gitignore index 08249d7..86d3a12 100644 --- a/.gitignore +++ b/.gitignore @@ -179,3 +179,7 @@ do_not_track/ # pixi environments .pixi/* !.pixi/config.toml +docs/_build +docs/source/api/generated +docs/source/api/api/* +docs/zz/* diff --git a/docs/source/Tutorial_calculate_calibration_beta_factor.md b/docs/source/Tutorial_calculate_calibration_beta_factor.md index 15637a2..f969ff8 100644 --- a/docs/source/Tutorial_calculate_calibration_beta_factor.md +++ b/docs/source/Tutorial_calculate_calibration_beta_factor.md @@ -18,6 +18,9 @@ from gsolve import ( DialToMgalConverter, GSolveReport ) + +from gsolve.tide.earth_tide import LongmanTidalCorrection +from gsolve.tide.earth_tide import EternaPredictTidalCorrection ``` Set up the directory and files required. We need a calibration survey dataset (in excel format here), the G meter dial correction table and the reference stations list. @@ -76,12 +79,25 @@ obs.apply_dial_to_mgal(g106converter) Note that as we want to calculate the calibration factor we do not need to apply an existing calibration. -### calculate the earth tide correction which requires location information from sites +### Calculate the earth tide correction + +This requires location information from the sites object. There is the option of using the Longman formula or ETERNA Predict with a choice of tidal potential models. +Refer to [pygtide documentation](https://github.com/hydrogeoscience/pygtide) for other options. ```python -obs.apply_earth_tide_correction(sites) +longman = LongmanTidalCorrection(amp_factor=1.2) +obs.apply_earth_tide_correction(sites, tide_corrector = longman) ``` +Another option is to use pygtide for corrections which uses the more accurate ETERNA tidal model. + +```python +eterna = EternaPredictTidalCorrection(tidalpoten=8) +obs.apply_earth_tide_correction(sites, tide_corrector = eterna) +``` + +Other tidal parameters for Eterna can be set through ```set_tidal_params``` once the tide correction object is initialised. e.g. ```eterna.set_tidal_params([0, 100, 1.15, 0])``` + ### calculate earth tide corrected gravity ```python diff --git a/docs/source/Tutorial_network_adjustment_and_anomaly_calculation.md b/docs/source/Tutorial_network_adjustment_and_anomaly_calculation.md index 9a2e660..11486a8 100644 --- a/docs/source/Tutorial_network_adjustment_and_anomaly_calculation.md +++ b/docs/source/Tutorial_network_adjustment_and_anomaly_calculation.md @@ -141,7 +141,7 @@ Plot the observed data for each loop to check for input errors. Here we plot fo obs.plot_observed_data(2, "datetime", "meter_reading_mgal") ``` -![observed data](Figure_1.png) +![observed data](_static/Figure_1.png) ### Plot a network map @@ -161,7 +161,7 @@ if has_contextily: fig.savefig(str(obs_path) + "/ Okataina_network_map.png") ``` -![network map](Okataina_network_map.png) +![network map](_static/Okataina_network_map.png) ### Create a survey object using observations and site objects @@ -200,8 +200,8 @@ results.plot_residual_drift(loop=2, filename=str(obs_path) + "/ Okataina_residua results.plot_residual_cdf(loop=2, filename=str(obs_path) + "/ Okataina_residual_cdf.png") ``` -![residual_drift](Figure_2.png) -![residual_cdf](Figure_3.png) +![residual_drift](_static/Figure_2.png) +![residual_cdf](_static/Figure_3.png) ### Save output files diff --git a/docs/source/Figure_1.png b/docs/source/_static/Figure_1.png similarity index 100% rename from docs/source/Figure_1.png rename to docs/source/_static/Figure_1.png diff --git a/docs/source/Figure_2.png b/docs/source/_static/Figure_2.png similarity index 100% rename from docs/source/Figure_2.png rename to docs/source/_static/Figure_2.png diff --git a/docs/source/Figure_3.png b/docs/source/_static/Figure_3.png similarity index 100% rename from docs/source/Figure_3.png rename to docs/source/_static/Figure_3.png diff --git a/docs/source/Okataina_network_map.png b/docs/source/_static/Okataina_network_map.png similarity index 100% rename from docs/source/Okataina_network_map.png rename to docs/source/_static/Okataina_network_map.png diff --git a/docs/source/terrain_correction.png b/docs/source/_static/terrain_correction.png similarity index 100% rename from docs/source/terrain_correction.png rename to docs/source/_static/terrain_correction.png diff --git a/docs/source/_templates/autosummary/class.rst b/docs/source/_templates/autosummary/class.rst new file mode 100644 index 0000000..0ebc2e3 --- /dev/null +++ b/docs/source/_templates/autosummary/class.rst @@ -0,0 +1,32 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + + {% block methods %} + + {% block attributes %} + {% if attributes %} + .. rubric:: {{ _('Attributes') }} + + .. autosummary:: + {% for item in attributes %} + {% if item in members and not item.startswith('_') %} + ~{{ name }}.{{ item }} + {% endif %} + {%- endfor %} + {% endif %} + {% endblock %} + + {% if methods %} + .. rubric:: {{ _('Methods') }} + + .. autosummary:: + {% for item in methods %} + {% if item in members and (not item.startswith('_') or item in ['__call__']) %} + ~{{ name }}.{{ item }} + {% endif %} + {%- endfor %} + {% endif %} + {% endblock %} diff --git a/docs/source/_templates/autosummary/class_without_autosummary.rst b/docs/source/_templates/autosummary/class_without_autosummary.rst new file mode 100644 index 0000000..b57a7ce --- /dev/null +++ b/docs/source/_templates/autosummary/class_without_autosummary.rst @@ -0,0 +1,6 @@ +{{ fullname }} +{{ underline }} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} \ No newline at end of file diff --git a/docs/source/api/anomalies_corrections.rst b/docs/source/api/anomalies_corrections.rst new file mode 100644 index 0000000..db945db --- /dev/null +++ b/docs/source/api/anomalies_corrections.rst @@ -0,0 +1,98 @@ +================================== +Standard Corrections and Anomalies +=================================== + +GravityAnomalies +================ +.. currentmodule:: gsolve + +.. autosummary:: + :toctree: api/ + + GravityAnomalies + GravityAnomalies.copy + GravityAnomalies.from_csv + GravityAnomalies.from_dataframe + GravityAnomalies.from_excel + GravityAnomalies.known_fields + GravityAnomalies.required_fields + GravityAnomalies.set_column + GravityAnomalies.write_to_csv + + +GravityCorrectionParameters +=========================== +.. currentmodule:: gsolve + +.. autosummary:: + :toctree: api/ + + GravityCorrectionParameters + GravityCorrectionParameters.from_series + GravityCorrectionParameters.copy + GravityCorrectionParameters.to_dict + GravityCorrectionParameters.to_excel + GravityCorrectionParameters.to_series + GravityCorrectionParameters.summary + GravityCorrectionParameters.bouguer_correction_fields + GravityCorrectionParameters.bouguer_correction_type + GravityCorrectionParameters.default_values + GravityCorrectionParameters.non_default_values + + +Functions +========= +.. currentmodule:: gsolve.reductions.anomalies + +.. autosummary:: + :toctree: api/ + + compute_complete_bouguer_anomaly + compute_free_air_anomaly + compute_simple_bouguer_anomaly + +GravityCorrectionProvider +========================= +.. currentmodule:: gsolve.reductions.corrections + +.. autosummary:: + :toctree: api/ + + GravityCorrectionProvider + GravityCorrectionProvider.compute + GravityCorrectionProvider.available_corrections + GravityCorrectionProvider.bouguer_corrections + GravityCorrectionProvider.free_air_corrections + +GravityCorrections +================== +.. currentmodule:: gsolve.reductions.corrections + +.. autosummary:: + :toctree: api/ + + GravityCorrections + GravityCorrections.copy + GravityCorrections.from_csv + GravityCorrections.from_dataframe + GravityCorrections.from_excel + GravityCorrections.known_fields + GravityCorrections.required_fields + GravityCorrections.set_column + GravityCorrections.write_to_csv + + +Functions +========= +.. currentmodule:: gsolve.reductions.corrections + +.. autosummary:: + :toctree: api/ + + atmospheric_correction + bouguer_slab_correction + bouguer_slab_curvature_corrected + free_air_correction + normal_gravity_at_ellipsoid + normal_gravity_at_stn_elevation + spherical_bouguer_cap_correction diff --git a/docs/source/api/api_index.rst b/docs/source/api/api_index.rst index b76c900..044cff5 100644 --- a/docs/source/api/api_index.rst +++ b/docs/source/api/api_index.rst @@ -1,34 +1,56 @@ +============= API Reference ============= -.. automodule:: gsolve - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: gsolve.tide.earth_tide - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: gsolve.tide.ocean_load - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: gsolve.scintrex - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: gsolve.reductions.corrections - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: gsolve.reductions.anomalies - :members: - :undoc-members: - :show-inheritance: +This section contains per-module API documentation. + +Observations and Site Data +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. toctree:: + :maxdepth: 1 + + observations + sites + survey + scintrex + meter_conversion + +Tidal Corrections +~~~~~~~~~~~~~~~~~ +.. toctree:: + :maxdepth: 1 + + tide_earth_tide + tide_ocean_load + +gSolve Algorithms +~~~~~~~~~~~~~~~~ +.. toctree:: + :maxdepth: 1 + + gsolve_algorithms + reports + +Reductions and Anomalies +~~~~~~~~~~~~~~~~~~~~~~~~ +.. toctree:: + :maxdepth: 1 + + anomalies_corrections + reductions_terrain_corrections + + +Utils and Internal Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. toctree:: + :maxdepth: 1 + + core_data + core_excel_io + core_utils + xarray_methods + core_typing + diff --git a/docs/source/api/core_data.rst b/docs/source/api/core_data.rst new file mode 100644 index 0000000..25d283a --- /dev/null +++ b/docs/source/api/core_data.rst @@ -0,0 +1,47 @@ +========================== +Base Classes and Protocols +========================== + +.. currentmodule:: gsolve.core.data + +DataFieldSpecification +====================== +.. autosummary:: + :toctree: api/ + + DataFieldSpecification + + +GSolveParameters +================ +.. currentmodule:: gsolve.core.data + +.. autosummary:: + :toctree: api/ + + GSolveParameters + GSolveParameters.copy + GSolveParameters.from_series + GSolveParameters.default_values + GSolveParameters.non_default_values + GSolveParameters.summary + GSolveParameters.to_dict + GSolveParameters.to_excel + GSolveParameters.to_series + +GSolveTable +=========== +.. currentmodule:: gsolve.core.data + +.. autosummary:: + :toctree: api/ + + GSolveTable + GSolveTable.copy + GSolveTable.from_csv + GSolveTable.from_dataframe + GSolveTable.from_excel + GSolveTable.known_fields + GSolveTable.required_fields + GSolveTable.set_column + GSolveTable.write_to_csv \ No newline at end of file diff --git a/docs/source/api/core_excel_io.rst b/docs/source/api/core_excel_io.rst new file mode 100644 index 0000000..1a2a753 --- /dev/null +++ b/docs/source/api/core_excel_io.rst @@ -0,0 +1,13 @@ +gsolve.core.excel_io +==================== + +.. currentmodule:: gsolve.core.excel_io + +.. autosummary:: + :toctree: api/ + + read_excel_worksheet + write_excel_worksheet + get_excel_worksheets + get_true_sheet_name + _parse_sheet_name_arg \ No newline at end of file diff --git a/docs/source/api/core_typing.rst b/docs/source/api/core_typing.rst new file mode 100644 index 0000000..27654a6 --- /dev/null +++ b/docs/source/api/core_typing.rst @@ -0,0 +1,7 @@ +Type Hinting and Protocols +========================== + +.. automodule:: gsolve.core._typing + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/api/core_utils.rst b/docs/source/api/core_utils.rst new file mode 100644 index 0000000..45aeb20 --- /dev/null +++ b/docs/source/api/core_utils.rst @@ -0,0 +1,60 @@ +Utility Functions and Classes +============================= + +GSolveDataWarning +================= +.. currentmodule:: gsolve.core.utils + +.. autosummary:: + :toctree: api/ + + GSolveDataWarning + GSolveDataWarning.count + GSolveDataWarning.final_msg + GSolveDataWarning.print_msgs + +========= +Functions +========= +.. currentmodule:: gsolve.core.utils + +Data Conversion +--------------- +.. autosummary:: + :toctree: api/ + + to_1d_ndarray + to_1d_ndarray_or_float + to_naive_utc_datetime + normalize_field_names + normalize_str + check_duplicate_index + +Excel Data Handling +--------------- +.. autosummary:: + :toctree: api/ + + columns_to_timestamp + timestamp_to_columns + merge_datetime_columns + expand_datetime_column + prepare_writable_df + +Survey Loop Handling +-------------------- +.. autosummary:: + :toctree: api/ + + generate_loop_intervals + generate_loop_names + identify_loop_blocks + loops_from_gaps + +Other Utilities +------------------- +.. autosummary:: + :toctree: api/ + + dms2rad + round_coords diff --git a/docs/source/api/gsolve_algorithms.rst b/docs/source/api/gsolve_algorithms.rst new file mode 100644 index 0000000..14612f5 --- /dev/null +++ b/docs/source/api/gsolve_algorithms.rst @@ -0,0 +1,55 @@ +============================= +gSolve Algorithms and Outputs +============================= + +gSolve Algorithm +================ + +.. currentmodule:: gsolve.gsolve_algorithms + +.. autosummary:: + :toctree: api/ + + call_gsolve_lstsq + g_solver_lstsq + +GSolveResults +============= + +Results of a gSolve network adjustment. + +.. currentmodule:: gsolve.gsolve_outputs + +.. autosummary:: + :toctree: api/ + + GSolveResults + GSolveResults.calibration_factor + GSolveResults.plot_residual_cdf + GSolveResults.plot_residual_drift + GSolveResults.set_inputs + GSolveResults.set_solutions + + +GSolveSolutionParameters +======================== + +Class for storing network adjustment parameters and solution metadata. + +.. currentmodule:: gsolve.gsolve_outputs + +.. autosummary:: + :toctree: api/ + + GSolveSolutionParameters + GSolveSolutionParameters.calculated_calibration_factor + GSolveSolutionParameters.copy + GSolveSolutionParameters.default_values + GSolveSolutionParameters.from_series + GSolveSolutionParameters.gsolve_run_datetime + GSolveSolutionParameters.gsolve_version + GSolveSolutionParameters.non_default_values + GSolveSolutionParameters.summary + GSolveSolutionParameters.to_dict + GSolveSolutionParameters.to_excel + GSolveSolutionParameters.to_series diff --git a/docs/source/api/meter_conversion.rst b/docs/source/api/meter_conversion.rst new file mode 100644 index 0000000..2ed4ce6 --- /dev/null +++ b/docs/source/api/meter_conversion.rst @@ -0,0 +1,31 @@ +================================ +LaCoste-Romberg Meter Conversion +================================ + +LaCosteRombergDialConverter +--------------------------- +.. currentmodule:: gsolve + +.. autosummary:: + :toctree: api/ + + LaCosteRombergDialConverter + LaCosteRombergDialConverter.convert_readings + LaCosteRombergDialConverter.meter_id + LaCosteRombergDialConverter.converter_id + LaCosteRombergDialConverter.starttime + LaCosteRombergDialConverter.endtime + LaCosteRombergDialConverter.set_datetime_range + LaCosteRombergDialConverter.from_csv + LaCosteRombergDialConverter.from_dataframe + +MeterReadingConverter +--------------------- +.. currentmodule:: gsolve.meter_conversion + +.. autosummary:: + :toctree: api/ + + MeterReadingConverter + MeterReadingConverter.convert_readings + MeterReadingConverter.converter_id diff --git a/docs/source/api/observations.rst b/docs/source/api/observations.rst new file mode 100644 index 0000000..ebc80c0 --- /dev/null +++ b/docs/source/api/observations.rst @@ -0,0 +1,109 @@ +====================================== +Gravity Observations +====================================== + + +GravityObservations +=================== + +A class for handling gravity observation data, including methods for data manipulation, +correction. + +.. currentmodule:: gsolve + +Object Creation +--------------- +.. autosummary:: + :toctree: api/ + + GravityObservations + GravityObservations.from_dataframe + GravityObservations.from_excel + GravityObservations.from_csv + +Data Attributes +--------------- +.. autosummary:: + :toctree: api/ + + GravityObservations.known_fields + GravityObservations.required_fields + GravityObservations.set_column + GravityObservations.set_obs_id + GravityObservations.loop_ids + GravityObservations.loop_summary + GravityObservations.site_summary + GravityObservations.params + GravityObservations.activate + GravityObservations.deactivate + GravityObservations.check_data + +Time Handing +------------ +.. autosummary:: + :toctree: api/ + + GravityObservations.starttime + GravityObservations.endtime + GravityObservations.timedelta_unit + GravityObservations.set_timedelta_unit + GravityObservations.fixed_time_datum + GravityObservations.set_fixed_time_datum + GravityObservations.set_tdelta + +Corrections +----------- +.. autosummary:: + :toctree: api/ + + GravityObservations.apply_dial_to_mgal + GravityObservations.set_calibration_factor + GravityObservations.apply_earth_tide_correction + GravityObservations.apply_ocean_load_correction + GravityObservations.calculate_tide_corrected_gravity + +Data Export +----------- +.. autosummary:: + :toctree: api/ + + GravityObservations.write_to_csv + GravityObservations.to_excel + +Plotting +-------- +.. autosummary:: + :toctree: api/ + + GravityObservations.plot_observed_data + GravityObservations.plot_network_map + GravityObservations.plot_site_visits + +GravityObservationsParameters +============================= +A class for storing parameters and metadata related to reductions of data +in a GravityObservations object. + +.. currentmodule:: gsolve.observations + +.. autosummary:: + :toctree: api/ + + GravityObservationsParameters + GravityObservationsParameters.from_series + GravityObservationsParameters.copy + GravityObservationsParameters.default_values + GravityObservationsParameters.non_default_values + GravityObservationsParameters.summary + GravityObservationsParameters.to_dict + GravityObservationsParameters.to_excel + GravityObservationsParameters.to_series + +Functions +========= +.. currentmodule:: gsolve.observations + +.. autosummary:: + :toctree: api/ + + combine_gravity_observations \ No newline at end of file diff --git a/docs/source/api/reductions_terrain_corrections.rst b/docs/source/api/reductions_terrain_corrections.rst new file mode 100644 index 0000000..6795ce0 --- /dev/null +++ b/docs/source/api/reductions_terrain_corrections.rst @@ -0,0 +1,73 @@ +=========================== +TerrainCorrectionParameters +=========================== +Set parameters for a terrain correction computation. + +.. currentmodule:: gsolve + +.. autosummary:: + :toctree: api/ + + TerrainCorrectionParameters + TerrainCorrectionParameters.from_dataframe + TerrainCorrectionParameters.from_series + + TerrainCorrectionParameters.summary + TerrainCorrectionParameters.default_values + TerrainCorrectionParameters.non_default_values + + TerrainCorrectionParameters.copy + TerrainCorrectionParameters.to_dict + TerrainCorrectionParameters.to_excel + TerrainCorrectionParameters.to_series + +================ +TerrainCorrector +================ +Compute terrain coorections for a set of stations. + +.. currentmodule:: gsolve + +.. autosummary:: + :toctree: api/ + + TerrainCorrector + TerrainCorrector.zones + TerrainCorrector.add_zone + TerrainCorrector.compute + +===================== +TerrainCorrectionData +===================== +Class to store and manage terrain corrections produced by a TerrainCorrector. + +.. currentmodule:: gsolve + +.. autosummary:: + :toctree: api/ + + TerrainCorrectionData + TerrainCorrectionData.create_empty + TerrainCorrectionData.from_excel + TerrainCorrectionData.from_csv + TerrainCorrectionData.from_dataframe + TerrainCorrectionData.copy + TerrainCorrectionData.set_corrections + TerrainCorrectionData.get_corrections + TerrainCorrectionData.known_fields + TerrainCorrectionData.required_fields + TerrainCorrectionData.to_csv + TerrainCorrectionData.to_excel + TerrainCorrectionData.write_to_csv + +========= +Functions +========= +.. currentmodule:: gsolve.reductions.terrain_corrections + +.. autosummary:: + :toctree: api/ + + calculate_terrain_correction + tcorr_harmonica_bathymetry + tcorr_harmonica_topography \ No newline at end of file diff --git a/docs/source/api/reports.rst b/docs/source/api/reports.rst new file mode 100644 index 0000000..3fa1e40 --- /dev/null +++ b/docs/source/api/reports.rst @@ -0,0 +1,11 @@ +============ +GSolveReport +============ +.. currentmodule:: gsolve + +.. autosummary:: + :toctree: api/ + + GSolveReport + GSolveReport.copy + GSolveReport.to_excel diff --git a/docs/source/api/scintrex.rst b/docs/source/api/scintrex.rst new file mode 100644 index 0000000..fdf591d --- /dev/null +++ b/docs/source/api/scintrex.rst @@ -0,0 +1,33 @@ +Scintrex Data Import +==================== + +CG6Data +------- +.. currentmodule:: gsolve.scintrex + +.. autosummary:: + :toctree: api/ + + CG6Data + CG6Data.copy + CG6Data.from_file + CG6Data.meter_id + CG6Data.set_drift_correction + CG6Data.set_loop + CG6Data.stations + CG6Data.to_gsolve_observations + CG6Data.to_gsolve_sites + +ScintrexData +------------ +.. currentmodule:: gsolve.scintrex + +.. autosummary:: + :toctree: api/ + + ScintrexData + ScintrexData.copy + ScintrexData.meter_id + ScintrexData.set_loop + ScintrexData.stations + ScintrexData.to_gsolve_observations diff --git a/docs/source/api/sites.rst b/docs/source/api/sites.rst new file mode 100644 index 0000000..9213800 --- /dev/null +++ b/docs/source/api/sites.rst @@ -0,0 +1,72 @@ +=================== +GravitySites +=================== +.. currentmodule:: gsolve + +Object Creation +~~~~~~~~~~~~~~~ +.. autosummary:: + :toctree: api/ + + GravitySites + GravitySites.from_excel + GravitySites.from_csv + GravitySites.from_dataframe + GravitySites.copy + GravitySites.set_column + + +Information +~~~~~~~~~~~ +.. autosummary:: + :toctree: api/ + + GravitySites.known_fields + GravitySites.required_fields + GravitySites.check_data + GravitySites.set_reference_gravity + GravitySites.activate_ties + GravitySites.deactivate_ties + GravitySites.get_points + GravitySites.get_ties + GravitySites.sample_elevation + +Data Export +~~~~~~~~~~~ +.. autosummary:: + :toctree: api/ + + GravitySites.to_excel + GravitySites.write_to_csv + +================ +ReferenceGravity +================ +.. currentmodule:: gsolve + +.. autosummary:: + :toctree: api/ + + ReferenceGravity + ReferenceGravity.copy + ReferenceGravity.from_csv + ReferenceGravity.from_dataframe + ReferenceGravity.from_dict + ReferenceGravity.from_excel + ReferenceGravity.known_fields + ReferenceGravity.required_fields + ReferenceGravity.set_column + ReferenceGravity.to_excel + ReferenceGravity.write_to_csv + +========= +Functions +========= +.. currentmodule:: gsolve.sites + +.. autosummary:: + :toctree: api/ + + combine_gravity_sites + combine_reference_gravity + diff --git a/docs/source/api/survey.rst b/docs/source/api/survey.rst new file mode 100644 index 0000000..88142e3 --- /dev/null +++ b/docs/source/api/survey.rst @@ -0,0 +1,29 @@ +========================= +Survey +========================= + +GravitySurvey +============= +.. currentmodule:: gsolve + +.. autosummary:: + :toctree: api/ + + GravitySurvey + GravitySurvey.apply_dial_to_mgal + GravitySurvey.apply_earth_tide_correction + GravitySurvey.calculate_tide_corrected_gravity + GravitySurvey.from_excel + GravitySurvey.pre_flight_check + GravitySurvey.set_calibration_factor + GravitySurvey.set_reference_gravity + GravitySurvey.solve_lstsq + +Functions +--------- +.. currentmodule:: gsolve.observations + +.. autosummary:: + :toctree: api/ + + combine_gravity_surveys diff --git a/docs/source/api/tide_earth_tide.rst b/docs/source/api/tide_earth_tide.rst new file mode 100644 index 0000000..ebbef69 --- /dev/null +++ b/docs/source/api/tide_earth_tide.rst @@ -0,0 +1,60 @@ +===================== +Earthtide Corrections +===================== + +LongmanTidalCorrection +====================== +.. currentmodule:: gsolve.tide.earth_tide + +.. autosummary:: + :toctree: api/ + + LongmanTidalCorrection + LongmanTidalCorrection.gravity_accelerations + LongmanTidalCorrection.identifier + LongmanTidalCorrection.tidal_correction + LongmanTidalCorrection.time_series + LongmanConstants + + +EternaPredictTidalCorrection +============================ +.. currentmodule:: gsolve.tide.earth_tide + +.. autosummary:: + :toctree: api/ + + EternaPredictTidalCorrection + EternaPredictTidalCorrection.identifier + EternaPredictTidalCorrection.set_tidal_params + EternaPredictTidalCorrection.tidal_correction + EternaPredictTidalCorrection.time_series + + +EternaTidalParameters +===================== +.. currentmodule:: gsolve.tide.earth_tide + +.. autosummary:: + :toctree: api/ + + EternaTidalParameters + EternaTidalParameters.copy + EternaTidalParameters.default + EternaTidalParameters.from_array + EternaTidalParameters.from_dataframe + EternaTidalParameters.from_excel + EternaTidalParameters.pygtide_wavegroup_arg + EternaTidalParameters.quick_tide_pro_defaults + EternaTidalParameters.sort_and_validate + +Functions +========= +.. currentmodule:: gsolve.tide.earth_tide + +.. autosummary:: + :toctree: api/ + + _decimal_hour_of_day + _decimal_julian_century + gravimetric_factor \ No newline at end of file diff --git a/docs/source/api/tide_ocean_load.rst b/docs/source/api/tide_ocean_load.rst new file mode 100644 index 0000000..7c55c0d --- /dev/null +++ b/docs/source/api/tide_ocean_load.rst @@ -0,0 +1,68 @@ +========================= +Ocean Loading Corrections +========================= + +.. currentmodule:: gsolve.tide.ocean_load + +HardispOceanLoadCorrector +========================= + +.. autosummary:: + :toctree: api/ + + HardispOceanLoadCorrector + HardispOceanLoadCorrector.identifier + HardispOceanLoadCorrector.ocean_load_correction + HardispOceanLoadCorrector.stations + +QuickTidePro Functions +====================== + +.. currentmodule:: gsolve.tide.ocean_load + +.. autosummary:: + :toctree: api/ + + generate_qtp_input + qtp_to_corrector + read_qtp_multistation + read_qtp_timeseries + + +OceanLoadTimeSeries +=================== + +.. currentmodule:: gsolve.tide.ocean_load + +.. autosummary:: + :toctree: api/ + + OceanLoadTimeSeries + OceanLoadTimeSeries.starttime + OceanLoadTimeSeries.sample_rate + OceanLoadTimeSeries.endtime + OceanLoadTimeSeries.identifier + OceanLoadTimeSeries.ocean_load_correction + + +OceanLoadAtSiteTime +=================== +.. currentmodule:: gsolve.tide.ocean_load + +.. autosummary:: + :toctree: api/ + + OceanLoadAtSiteTime + OceanLoadAtSiteTime.identifier + OceanLoadAtSiteTime.ocean_load_correction + +OceanLoadCorrectionProvider +=========================== +.. currentmodule:: gsolve.tide.ocean_load + +.. autosummary:: + :toctree: api/ + + OceanLoadCorrectionProvider + OceanLoadCorrectionProvider.identifier + OceanLoadCorrectionProvider.ocean_load_correction diff --git a/docs/source/api/xarray_methods.rst b/docs/source/api/xarray_methods.rst new file mode 100644 index 0000000..c1f960f --- /dev/null +++ b/docs/source/api/xarray_methods.rst @@ -0,0 +1,47 @@ +============== +XArray Methods +============== + +DEM Functions +============= +.. currentmodule:: gsolve.core.xr_methods + +.. autosummary:: + :toctree: api/ + + load_dem + prepare_dem + check_dem + create_empty_dataarray + +Xarray Accessor Class and Methods +================================= +.. currentmodule:: gsolve.core.xr_accessor + +.. autosummary:: + :toctree: api/ + + TCorrMethods + TCorrMethods.xc + TCorrMethods.yc + TCorrMethods.xdim + TCorrMethods.ydim + TCorrMethods.dx + TCorrMethods.dy + TCorrMethods.bounds + TCorrMethods.cell_edges + TCorrMethods.coords_to_indices + + TCorrMethods.is_compatible + TCorrMethods.clip_to_arr + TCorrMethods.clip_to_points + + TCorrMethods.get_land_sea_mask + TCorrMethods.generate_distance_mask + TCorrMethods.apply_mask + + TCorrMethods.generate_bathymetry_density + TCorrMethods.generate_topo_density + TCorrMethods.get_bathymetry_elevation + TCorrMethods.get_topography_elevation + diff --git a/docs/source/conf.py b/docs/source/conf.py index de6fca8..688d464 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -61,11 +61,16 @@ "sphinx_copybutton", "sphinx_design", ] + +# numpydoc configuration numpydoc_show_class_members = False +numpydoc_show_inherited_class_members = False +numpydoc_attributes_as_param_list = False +# numpydoc_validation_checks = {"all", "EX01" "SA01"} +numpydoc_validation_checks = set() # Disable all validation checks myst_enable_extensions = ["amsmath"] -templates_path = ["_templates"] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] source_suffix = { @@ -74,18 +79,24 @@ } autosummary_generate = True +autosummary_generate_overwrite = True + +templates_path = ["_templates"] + # -- Options for autodoc ----------------------------------------------------- autodoc_default_options = { - "members": True, + "members": False, "undoc-members": True, "show-inheritance": True, } +autodoc_typehints = "none" # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = "sphinx_book_theme" html_title = "gSolve" +html_title = "gSolve" html_logo = "_static/gsolve_logo.png" html_static_path = ["_static"] html_theme_options = { @@ -102,4 +113,8 @@ html_context = { "display_version": True, # Ensure version is displayed "version": release, # Pass version variable to templates + "version": release, # Pass version variable to templates } + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" \ No newline at end of file diff --git a/docs/source/fundamentals.md b/docs/source/fundamentals.md index e143191..2bdbb50 100644 --- a/docs/source/fundamentals.md +++ b/docs/source/fundamentals.md @@ -2,7 +2,7 @@ [Gsolve](index.rst) has four main data objects called [`GravityObservations`](#observations), [`GravitySites`](#sites), [`ReferenceGravity`](#reference-gravity) and [`GravitySurvey`](#surveys). -For processing beyond a network adjustment (i.e. to Complete Bouguer anomaly) there are [`GravityCorrections`](#gravity-corrections), [`TerrainCorrector`](#Terrain corrections) and [`GravityAnomalies`](#gravity-anomalies) and finally a [`GSolveReports`](#output-files) class for output results files. +For processing beyond a network adjustment (i.e. to Complete Bouguer anomaly) there are [`GravityCorrections`](#gravity-corrections), [`TerrainCorrector`](terrain_corrections.md) and [`GravityAnomalies`](#gravity-anomalies) and finally a [`GSolveReports`](#output-files) class for output results files. A [command line interface](gsolve_cli_manual.md) is available for network adjustment processing. diff --git a/docs/source/requirements.txt b/docs/source/requirements.txt deleted file mode 100644 index 81ffd88..0000000 --- a/docs/source/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -sphinx-autoapi==3.2.1 -myst-parser==4.0.0 -sphinx-book-theme -sphinx-gallery -sphinx-design -sphinx -sphinx-copybutton -numpydoc diff --git a/docs/source/terrain_corrections.md b/docs/source/terrain_corrections.md index 9374715..8821c43 100644 --- a/docs/source/terrain_corrections.md +++ b/docs/source/terrain_corrections.md @@ -10,7 +10,7 @@ Gravity values are calculated at the exact coordinate location supplied. The figure below illustrates how prisms are constructed and assigned densities. -![prism_setup](terrain_correction.png) +![prism_setup](_static/terrain_correction.png) Terrain correction calculations in gSolve works by estimating the gravity effect of the topography in a series of zones around the observation site. Each zone has a minimum and maximum distance. diff --git a/examples/scripts/TeMaari_ocean_loading_pyhardisp.py b/examples/scripts/TeMaari_ocean_loading_pyhardisp.py index f8a33e8..591be38 100644 --- a/examples/scripts/TeMaari_ocean_loading_pyhardisp.py +++ b/examples/scripts/TeMaari_ocean_loading_pyhardisp.py @@ -31,9 +31,11 @@ ReferenceGravity, ) from gsolve.reports import GSolveReport -from gsolve.tide.earth_tide import LongmanTidalCorrection +from gsolve.tide.earth_tide import EternaPredictTidalCorrection from gsolve.tide.ocean_load import HardispOceanLoadCorrector +import numpy as np + # %% data_path = pathlib.Path(__file__).parent.parent @@ -46,7 +48,7 @@ corr_table_file = data_path / "correction_tables" / "G106.csv" # the calibration factor for your meter (determined in calibration survey) -calibration_factor = 1 - -0.0019 +calibration_factor = 1.0019 # %% # Read in observations @@ -77,8 +79,8 @@ obs.set_calibration_factor(calibration_factor) # calculate the earth tide correction which requires location information from sites -longman = LongmanTidalCorrection(amp_factor=1.16) -obs.apply_earth_tide_correction(sites, tide_corrector=longman) +eterna = EternaPredictTidalCorrection(np.c_[0, 10, 1.154, 0]) +obs.apply_earth_tide_correction(sites, tide_corrector=eterna) # Ocean Load Corrections using pyhardisp # read in the BLQ file from the online provider diff --git a/pyproject.toml b/pyproject.toml index 137872f..d5ec770 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "gsolve" -dynamic = ["version"] # version = "5.6.0" +dynamic = ["version"] # version = "5.6.0" description = "A Python package for processing gravity survey data." requires-python = ">=3.12" authors = [ @@ -28,6 +28,7 @@ dependencies = [ "boule==0.6.*", "harmonica<0.8", "pandas[excel,plot]<3", + "pydata-sphinx-theme>=0.16.1", "pygtide>=0.8.2", "pyhardisp>=0.2.1", "rioxarray>=0.22.0", @@ -69,10 +70,12 @@ docs = [ dev = [ "burocrata>=0.3.0", "ipykernel>=7.2.0", + "ipywidgets>=8.1.8", + "pandas-vet>=2023.8.2", "pytest>=9.0.3", "pytest-cov>=7.1.0", "pytest-datadir>=1.8.0", - "ruff" + "ruff", ] [tool.uv] @@ -108,9 +111,8 @@ extend-include = ["*.ipynb"] # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or # McCabe complexity (`C901`) by default. -select = ["ANN"] -# "D", "DOC"] -ignore = ["ANN101", "ANN102", "ANN003", "D105"] +select = ["ANN", "D", "DOC", "PD", "NPY"] +ignore = ["ANN101", "ANN102", "ANN003", "D105", "DOC502", "DOC501"] preview = true [tool.ruff.lint.per-file-ignores] @@ -145,3 +147,6 @@ include = ["gsolve/", "examples/scripts/"] [tool.setuptools.packages.find] where = ["src"] + +[tool.numpydoc_validation] +checks = ["all", "EX01", "SA01"] diff --git a/src/gsolve/core/data.py b/src/gsolve/core/data.py index eee5bdb..045e032 100644 --- a/src/gsolve/core/data.py +++ b/src/gsolve/core/data.py @@ -34,7 +34,7 @@ Renamer, ) from gsolve.core.excel_io import ( - parse_sheet_name_arg, + _parse_sheet_name_arg, read_excel_worksheet, write_excel_worksheet, ) @@ -145,22 +145,22 @@ def __len__(self) -> int: return len(self.data) if self else 0 def __copy__(self) -> Self: - """Ensure all copies are deep copies.""" + """Ensure all copies are deep copies.""" # noqa: DOC201 return deepcopy(self) def copy(self) -> Self: - """Return a deep copy of object.""" + """Return a deep copy of object.""" # noqa: DOC201 return self.__copy__() @classmethod def known_fields(cls) -> list[str]: - """Return a list of known fields in a GSolveTable or subclass.""" + """Return a list of known fields in the object.""" # noqa: DOC201 fields = [str(k) for k in getattr(cls, "_known_fields", {}).keys()] return fields @classmethod def required_fields(cls) -> list[str]: - """Return a list of required fields in a GSolveTable or subclass.""" + """Return a list of required fields in the object.""" # noqa: DOC201 if cls.known_fields(): return [k for k, v in getattr(cls, "_known_fields").items() if v.required] return [] @@ -195,7 +195,6 @@ def set_column( dtype : Any, default None The data type of the column. Defaults to None, do """ - for attrname in ("_custom_fields", "_known_fields"): fields = getattr(self, attrname, {}) if label in fields: @@ -223,7 +222,7 @@ def set_column( self.data[label] = _pd.Series(data=_data, index=self.data.index, dtype=dtype) def _data_ok(self, warn: bool = True) -> bool: - """Test whether data are complete according to specifications in ``obj._known_fields``.""" + """Test whether data are complete according to specifications in ``obj._known_fields``.""" # noqa: DOC201 rval = True for f in self.required_fields(): if f not in self.data.columns: @@ -256,7 +255,7 @@ def from_dataframe( use_index : bool, default True Load dataframe index as a data column. Drop index if False. ignore_unknown_fields : bool, default False - Ignore fields that are not in the `known_fields` attribute. + Ignore fields that are not in the known_fields attribute. parse_split_datetime: bool, default False If True, parse discrete year, month, day columns into a single datetime column and drop the original columns. Expected columns @@ -264,7 +263,7 @@ def from_dataframe( with at least year, month, and day being required. mapper : dict-like or function, default None Rename input fields/columns prior to loading. See - `pandas.Dataframe.rename` for details. + ``pandas.DataFrame.rename`` for details. Returns ------- @@ -315,7 +314,7 @@ def from_csv( csv_file : str or PathLike The path to the CSV file. ignore_unknown_fields : bool, default True - Only include `known fields` in the resulting object. + Only include known fields in the resulting object. parse_split_datetime: bool, default False If True, parse discrete year, month, day columns into a single datetime column and drop the original columns. Expected columns @@ -323,9 +322,9 @@ def from_csv( with at least year, month, and day being required. mapper : dict-like or function, default None Rename input fields/columns prior to loading. See - `pandas.Dataframe.rename` for details. - **kwargs - Additional keyword arguments to be passed to `pandas.read_csv`. + ``pandas.DataFrame.rename`` for details. + kwargs + Additional keyword arguments to be passed to ``pandas.read_csv``. Returns ------- @@ -356,11 +355,11 @@ def from_excel( ---------- excel_file : str or PathLike The path to the Excel file. - sheet_name : str | int | list-like, optional + sheet_name : str, int, or list-like, optional The name or index of the worksheet to read. If None, then try to use the default sheet name(s) defined in the class. ignore_unknown_fields : bool, default True - Only include `known fields` in the resulting object. + Only include known fields in the resulting object. parse_split_datetime: bool, default False If True, parse discrete year, month, day columns into a single datetime column and drop the original columns. Expected columns @@ -368,9 +367,9 @@ def from_excel( with at least year, month, and day being required. mapper : dict-like or function, default None Rename fields/columns after loading. See - `pandas.Dataframe.rename` for details. - **kwargs - Additional keyword arguments to be passed to `pandas.read_excel`. + ``pandas.DataFrame.rename`` for details. + kwargs + Additional keyword arguments to be passed to ``pandas.read_excel``. Returns ------- @@ -421,14 +420,16 @@ def write_to_csv( ---------- fname : str or PathLike Output file name. - normalize_columns_names : bool, default True + normalize_column_names : bool, default True Make column names lowercase with no spaces. expand_datetime : str or None, default None - Expand datetime fields to + Expand datetime fields to separate columns for year, month, day, ... + drop_datetime : bool, default False + Drop datetime fields from output. bool_to_int : bool, default False - Convert True, False to 1, 0. - **kwargs - Optional arguments to be passed to `pandas.to_csv`. + Convert True/False to 1/0. + kwargs + Optional arguments to be passed to ``pandas.to_csv``. See Also -------- @@ -479,18 +480,19 @@ class GSolveParameters: """Base class to store parameters related to GSolveTable derived classes.""" def __copy__(self) -> Self: - """Ensure all copies are deep copies.""" + # Ensure all copies are deep copies. return deepcopy(self) def __param_str__(self) -> str: + # Return a string representation of the parameters return repr(self).partition("(")[2].rpartition(")")[0] def copy(self) -> Self: - """Return a deep copy of object.""" + """Return a deep copy of object.""" # noqa: DOC201 return self.__copy__() def to_dict(self) -> dict: - """Return parameters as a dict.""" + """Return parameters as a dict.""" # noqa: DOC201 return dataclasses.asdict(self) def to_series( @@ -577,13 +579,13 @@ def from_series( @classmethod def default_values(cls) -> dict: - """Return dict of default parameter values.""" + """Return dict of default parameter values.""" # noqa: DOC201 return { k: cls.__dataclass_fields__[k].default for k in cls.__dataclass_fields__ } def non_default_values(self) -> dict: - """Return dict of non-default parameter values.""" + """Return dict of non-default parameter values.""" # noqa: DOC201 defaults = self.default_values() return {k: v for k, v in self.to_dict().items() if defaults.get(k, None) != v} @@ -615,12 +617,13 @@ def to_excel( Set the header label for parameter names column in output worksheet. values_label : str, default is 'value' Set the header label for parameter names column in output worksheet. - **kwargs : dict - Additional keyword arguments passed to `pandas.DataFrame.to_excel`. + kwargs : dict + Additional keyword arguments passed to ``pandas.DataFrame.to_excel``. See Also -------- - write_excel_worksheet + write_excel_worksheet : Function to write a DataFrame to an Excel worksheet + with options for handling existing workbooks and sheets. pandas.Dataframe.to_excel """ @@ -644,10 +647,26 @@ def to_excel( **kwargs, ) + # Todo: remove this method def summary( self, include_name: bool = True, as_list: bool = True ) -> list[str] | str: - """Return a list of params as strings in the form 'param: value'.""" + """ + Return parameters as strings in the form 'param: value'. + + Parameters + ---------- + include_name : bool, default True + Include the class name in the output. + as_list : bool, default True + Return the output as a list of strings. If False, return as a single string + with each parameter on a new line. + + Returns + ------- + list[str] | str + The parameters as a string or list of strings. + """ txt = [] if include_name: txt.append(f"{type(self).__name__}") diff --git a/src/gsolve/core/excel_io.py b/src/gsolve/core/excel_io.py index 069db06..2319cb4 100644 --- a/src/gsolve/core/excel_io.py +++ b/src/gsolve/core/excel_io.py @@ -36,18 +36,20 @@ ] -def get_excel_worksheets(excel_file: FilePath, **kwargs) -> list[str | int]: - """Return list of all worksheets in ``excel_file`` +def get_excel_worksheets(excel_file: FilePath, **kwargs) -> list[str]: + """Return list of all worksheets in ``excel_file``. Parameters ---------- excel_file : The excel workbook to read. + kwargs : + Additional arguments passed to ``pandas.ExcelFile`` class constructor. Returns ------- list - List of worksheet names in the excel file. + List of worksheet names in ``excel_file``. """ with _pd.ExcelFile(excel_file, **kwargs) as xls: return xls.sheet_names @@ -55,53 +57,53 @@ def get_excel_worksheets(excel_file: FilePath, **kwargs) -> list[str | int]: def get_true_sheet_name( excel_file: FilePath, sheet_name: str | int, raise_error: bool = False -) -> str | int | None: +) -> str | None: """ - For given `sheet_name` find the corresponding worksheet in `excel_file` ignoring - capitalisation etc. + Find ``sheet_name`` in ``excel_file`` by index or case-insensitive name. Parameters ---------- excel_file : str or PathLike The path to the excel file. sheet_name : str or int - The name or index of the sheet to read. + The name or index (0-based) of the sheet to locate. raise_error : bool, default is False - Raise an error if the sheet is not found, otherwise return None. + Raise a ValueError if the sheet is not found, otherwise return None. Returns ------- str or None The true sheet name if found, otherwise None. """ - true_sheet_name = None sheet_names = get_excel_worksheets(excel_file) if isinstance(sheet_name, int): try: - true_sheet_name = sheet_names[sheet_name] + return sheet_names[sheet_name] except IndexError: if raise_error: raise ValueError( f"excel file {excel_file} has no sheet at index: {sheet_name}" ) + else: + return None else: sheet_names_lc = [ s.lower() for s in get_excel_worksheets(excel_file) if isinstance(s, str) ] if sheet_name.lower() in sheet_names_lc: - true_sheet_name = sheet_names[sheet_names_lc.index(sheet_name.lower())] + return sheet_names[sheet_names_lc.index(sheet_name.lower())] elif raise_error: raise ValueError( f"excel file {excel_file} has no sheet named '{sheet_name}'" ) - - return true_sheet_name + else: + return None -def parse_sheet_name_arg( +def _parse_sheet_name_arg( sheet_name: str | int | Sequence[str | int], ) -> list[str | int]: - """Parse and validate sheet_name argument.""" + """Parse and validate sheet_name argument.""" # noqa: DOC201 sheet_name_list: list[str | int] if is_list_like(sheet_name): sheet_name_list = [s for s in sheet_name] # pyrefly:ignore[not-iterable] @@ -128,17 +130,17 @@ def read_excel_worksheet( **kwargs, ) -> _pd.DataFrame: """ - Read the excel worksheet `sheet_name` from `excel_file` into a DataFrame. + Read the excel worksheet ``sheet_name`` from ``excel_file`` into a DataFrame. Parameters ---------- excel_file : FilePath - The path to the excel file. + The excel/spreadsheet file to read. sheet_name : str or int or list-like The name (str) or index (int) of the worksheet to read from ``excel_file``. If multiple names/indices are provided, then read the first one found in ``excel_file``. - **kwargs + kwargs Additional arguments passed to ``pandas.read_excel`` method. Returns @@ -152,8 +154,7 @@ def read_excel_worksheet( The underlying function used to read the excel file. """ - - sheet_name_list = parse_sheet_name_arg(sheet_name) + sheet_name_list = _parse_sheet_name_arg(sheet_name) true_sheet_name = None for sname in sheet_name_list: @@ -180,7 +181,7 @@ def write_excel_worksheet( **kwargs, ) -> None: """ - Write a DataFrame to excel workbook `excel_file` in the worksheet `sheet_name`. + Write DataFrame to excel workbook ``excel_file`` in the worksheet ``sheet_name``. Parameters ---------- @@ -191,26 +192,26 @@ def write_excel_worksheet( sheet_name : str The name of the worksheet to write to. if_workbook_exists: {"error", "replace", "append"}, default "error" - Behaviour if the excel file already exists. + Behaviour if ``excel_file`` already exists. - "error": raise a ValueError. - "replace": replace the existing file. - "append": append to the existing file. if_sheet_exists: {"error", "replace", "new"}, default "error" - Behaviour if the worksheet already exists. Only applicable when - `if_workbook_exists='append'`. + Behaviour if the ``sheet_name`` already exists in ``excel_file`` + (append mode only) - "error": raise a ValueError. - "replace": replace the existing worksheet. - - "new": create a new worksheet - **kwargs + - "new": create a new worksheet with different name + kwargs Additional arguments passed to ``pandas.DataFrame.to_excel`` method. See Also -------- pandas.DataFrame.to_excel The underlying function used to write the DataFrame to the excel file. + pandas.ExcelWriter """ - if if_workbook_exists not in get_args(IfWorkbookExists): raise ValueError( f"invalid value for {if_workbook_exists=}, must be one of " @@ -224,7 +225,7 @@ def write_excel_worksheet( ) writer_kwargs: dict[str, Any] = { - "engine": "openpyxl", # "xlsxwriter", "openpyxl", "xlwt" + # "engine": "openpyxl", # "xlsxwriter", "openpyxl", "xlwt" "if_sheet_exists": if_sheet_exists, "mode": "w", } diff --git a/src/gsolve/core/utils.py b/src/gsolve/core/utils.py index 4df154e..16fbfa0 100644 --- a/src/gsolve/core/utils.py +++ b/src/gsolve/core/utils.py @@ -101,7 +101,7 @@ def to_naive_utc_datetime( **kwargs, ) -> pd.Timestamp | pd.Series | pd.DatetimeIndex | NaTType: """ - Convert inputs to UTC time, but with timezone information set to None + Convert inputs to UTC time, but with timezone information set to None. Datetime-like inputs that are timezone-aware are converted to UTC and then stripped of their timezone information. All other inputs are @@ -114,7 +114,7 @@ def to_naive_utc_datetime( allow_nat : bool, default True If False, raise ValueError if any input resolves to NaT. kwargs : - Additional keyword arguments to be passed to pandas.to_datetime(). + Additional keyword arguments to be passed to ``pandas.to_datetime()``. Returns ------- @@ -209,6 +209,7 @@ def to_1d_ndarray_or_float(a: ArrayLike) -> NDArray[np.float64] | np.float64: # Remove in future release def check_duplicate_index(idx: pd.Index | pd.DataFrame | pd.Series) -> None: + """Raise a ValueError if the index contains duplicate values.""" if isinstance(idx, (pd.DataFrame, pd.Series)): _idx = idx.index elif isinstance(idx, pd.Index): @@ -234,18 +235,20 @@ def normalize_field_names(df: pd.Series) -> pd.Series: ... def normalize_field_names(df: pd.DataFrame | pd.Series) -> pd.DataFrame | pd.Series: """ - Normalize DataFrame column names by converting to lower case str and replacing - spaces with underscores. + Return a copy of ``df`` with column values and axis.names converted to snake_case. + + If ``df`` is a DataFrame, ``df.columns``, ``df.columns.name``, and ``df.index.name`` + are normalized. If ``df`` is a Series, then only ``df.name`` and ``df.index.name`` are normalized. Parameters ---------- - df : pandas.DataFrame - The DataFrame to normalize. + df : DataFrame or Series + The object to normalize. Returns ------- - pandas.DataFrame - Copy of `df` with normalized column names. + pandas.DataFrame or pandas.Series + The same type as ``df``. """ if isinstance(df, pd.DataFrame): return df.rename(columns=normalize_str).rename_axis( @@ -262,7 +265,18 @@ def normalize_field_names(df: pd.DataFrame | pd.Series) -> pd.DataFrame | pd.Ser def normalize_str(s: str | int | float | bool | None) -> str | None: - """Convert `s` to lowercase and replace spaces with underscores.""" + """Convert ``s`` to str and format it to snake_case. + + Parameters + ---------- + s : str, int, float, bool, or None + The value to convert and normalize. Do nothing if ``s`` is None + + Returns + ------- + str or None + The normalized string or None if the input was None. + """ if s is not None: return str(s).strip().lower().replace(" ", "_") return None @@ -297,7 +311,7 @@ def merge_datetime_columns( drop: bool = False, **kwargs, ) -> pd.DataFrame: - """Merge discrete datetime columns into `pandas.Timestamp`. + """Merge discrete datetime columns into ``pandas.Timestamp``. Parameters ---------- @@ -309,13 +323,13 @@ def merge_datetime_columns( An ordered sequence of column names representing the date and time components to be parsed. The default is ("year", "month", "day", "hour", "minute", "second", "microsecond"). - `ts_columns` is parsed in order from largest time unit (year) down - to the smallest (microseconds). `ts_columns` must define at least + ``ts_columns`` is parsed in order from largest time unit (year) down + to the smallest (microseconds). ``ts_columns`` must define at least year, month, and day columns, and no 'gaps' are permitted. drop : bool, default = False Drop the date and time columns after conversion. - **kwargs - Additional keyword arguments to be passed to pd.to_datetime(). + kwargs + Additional keyword arguments to be passed to ``pandas.to_datetime()``. Returns ------- @@ -345,32 +359,33 @@ def columns_to_timestamp( **kwargs, ) -> pd.Series: """ - Generate a Series of `pandas.Timestamp` from discrete date & time columns in `df`. + Generate a Series of Timestamps from discrete date & time columns in ``df``. Parameters ---------- df : pandas.DataFrame The DataFrame containing the columns to be converted. - ts_columns : array-like, optional + ts_columns : array-like of str, optional An ordered sequence of column names representing the date and time - components to be parsed. The default names are: - ("year", "month", "day", "hour", "minute", "second", "microsecond"). - `ts_columns` is parsed in order from largest time unit (year) down - to the smallest (microseconds). `ts_columns` must at define at least - year, month, and day columns, and no 'gaps' are permitted. - **kwargs - Additional keyword arguments passed to `pandas.to_datetime`. + components to be parsed. The elements correspond to the default columns + ('year', 'month', 'day', 'hour', 'minute', 'second', 'microsecond'). + ``ts_columns`` is evaluated in order from largest time unit (year) down to + (microseconds). ``ts_columns`` must at define at least 'year', + 'month', and 'day' columns. E.g. ts_columns = ['yy', 'mm', 'dd', 'HH', 'MM', "SS"] + kwargs + Additional keyword arguments passed to ``pandas.to_datetime``. Returns ------- pandas.Series + Series of pandas.Timestamp objects. See Also -------- merge_datetime_columns timestamp_to_columns - The inverse function to `columns_to_timestamp`. + The inverse function to ``columns_to_timestamp``. pandas.to_datetime The underlying function used to convert the columns to a timestamp. """ @@ -411,29 +426,29 @@ def columns_to_timestamp( def timestamp_to_columns( ds: pd.Series | pd.DatetimeIndex, resolution: AllowedTimestampResolution | None = "second", - round_method: AllowedTimestampRoundingMethods = "floor", + round_method: AllowedTimestampRoundingMethods = "round", fill_nat: int | None = None, prefix: str = "", ) -> pd.DataFrame: """ - Convert a Series of `pandas.Timestamp` to discrete date & time columns. + Convert a Series of ``pandas.Timestamp`` to discrete date & time columns. Parameters ---------- ds : pandas.Series The Series or array like containing the timestamps to be converted. resolution : default "second" - Truncate output columns to 'resolution'. The truncation method is - round_method : {"floor", "ceil", "round"}, default "round" + Truncate output columns to ``resolution``. The truncation method is + round_method : {"round", "floor", "ceil"}, default "round" Control how datetimes are truncated to the specified resolution. - The default 'round' is appropriate where `resolution` is a time increment. - 'floor' might be a better choice where `resolution` is 'year', 'month' or 'day'. + The default 'round' is appropriate where ``resolution`` is a time increment. + 'floor' might be a better choice where ``resolution`` is a date increment. fill_nat : int, default = None - If not None, fill `pandas.NaT` values with `fill_nat`. NaT's are + If not None, fill ``pandas.NaT`` values with ``fill_nat``. NaT's are correctly split to NaN's, hoever a side effect is that the dataframe dtype will be `float` dtype rather than `int` prefix : str, default = "" - Prepend `prefix` to output column names. This is useful when + Prepend ``prefix`` to output column names. This is useful when splitting multiple datetime columns, to ensure that the output columns are uniquely named. @@ -447,7 +462,6 @@ def timestamp_to_columns( columns_to_timestamp expand_datetime_column """ - if not is_datetime64_any_dtype(ds): raise TypeError("Input data are not datetime-like") if isinstance(ds, pd.Index): @@ -508,7 +522,7 @@ def expand_datetime_column( """Expand datetime column(s) to discrete date and time component columns. The output columns will be named 'year', 'month', 'day', 'hour', 'minute', 'second', - 'microsecond', 'nanosecond', depending on the specied `resolution` and `prefix` + 'microsecond', 'nanosecond', depending on the specied ``resolution`` and ``prefix`` parameters. This method facillitates the export of data for reading by Microsoft Excel and @@ -524,23 +538,23 @@ def expand_datetime_column( The columns to be split. If column_name not specified, split all datetime columns. drop : bool, default is False - Drop `name` column after conversion. + Drop ``column_name`` column after conversion. resolution : {'year', 'month', 'day', 'hour', 'minute', 'second', 'microsecond', 'nanosecond'}, default is "second" Output datetime components down to specified resolution. round_method : {'floor', 'ceil', 'round'}, default is 'round': Control how datetimes are truncated to the specified resolution. The default - 'round' is appropriate where `resolution` is a time increment. 'floor' - might be a better choice where `resolution` is 'year', 'month' or 'day'. + 'round' is appropriate where ``resolution`` is a time increment. 'floor' + might be a better choice where ``resolution`` is 'year', 'month' or 'day'. prefix : str, list-like, default is "" - Prepend `prefix` to output column names. Behaviour depends on how many columns + Prepend ``prefix`` to output column names. Behaviour depends on how many columns are to be split:: - - If `prefix` is empty and 1 column to split, then use standard column names + - If ``prefix`` is empty and 1 column to split, then use standard column names "year", "month",... - _ If `prefix` is empty and multiple columns to split, then prepend source + - If ``prefix`` is empty and multiple columns to split, then prepend source column name to output "ColA_year", "ColA_month"..., "ColB_year", ... etc. - - If `prefix` is defined, then a `prefix` must be specified for each column + - If ``prefix`` is defined, then a ``prefix`` must be specified for each column to split insert_after : bool, default is True If True, insert new columns after the column being split. If False, @@ -657,14 +671,14 @@ def prepare_writable_df( Name of the column holding datetime to expand. datetime_resolution : str | None, default 'second' The resolution to which datettime column are to be epaneded. See - `expand_datetime_column` for explanation. + ``expand_datetime_column`` for explanation. datetime_round_method : {'floor', 'ceil', 'round'}, default 'floor' Control how datetimes are truncated to the specified resolution. - See `expand_datetime_column` for explanation. + See ``expand_datetime_column`` for explanation. datetime_prefix : str, default "" Prefix to prepend to expanded datetime column names. drop_datetime : bool, default True - If True and `expand_datetime` is not None, drop the expanded + If True and ``expand_datetime`` is not None, drop the expanded datetime column from output DataFrame. bool_to_int : bool, default True Whether to convert boolean columns to integers {False: 0, True: 1}. @@ -735,10 +749,10 @@ def __init__( self.messages: list[str] = [] @property - def count(self) -> int: + def count(self) -> int: # noqa: D102 return len(self.messages) - def __call__(self, msg: str) -> None: + def __call__(self, msg: str) -> None: # noqa: D102 self.messages.append(msg) self._display(msg) @@ -751,7 +765,7 @@ def print_msgs(self) -> None: for msg in self.messages: print(f"{self.prefix}: {msg}") - def final_msg(self) -> None: + def final_msg(self) -> None: # noqa: D102 if self.count > 0: self._display(f"{self.count} problem(s) encountered") @@ -763,15 +777,14 @@ def generate_loop_intervals( Parameters ---------- - datetime_bounds : array - Array of sorted datetime-like values that define interval boundaries. + datetime_bounds : array-like of datetime-like + Array of sorted datetime-like values defining interval boundaries. Returns ------- - pd.IntervalIndex + pandas.IntervalIndex Interval index object. """ - db = to_naive_utc_datetime(datetime_bounds, allow_nat=False) if not isinstance(db, (pd.Series, pd.DatetimeIndex)) or len(db) < 2: raise ValueError( @@ -886,13 +899,13 @@ def generate_loop_names( step: int = 1, format_str: str = "{LOOP}", ) -> list[str]: - """Generate loop idetifiers strings + """Generate loop identifier strings. Parameters ---------- n : int or array-like Number of loop identifiers to generate if ``n`` is an integer. If ``n`` is an - array-like object, then genera number of loop identifiers is set to the length of ``n``. + array-like object, then the number of loop identifiers is set to the length of ``n``. start : int, default 1 Loop identifier start value. step : int, default 1 @@ -910,20 +923,20 @@ def generate_loop_names( def round_coords(arr: np.typing.ArrayLike) -> np.ndarray: - """Round values in `arr`, with halfway cases rounded towards positive inifinty. + """Round values in ``arr``, with halfway cases rounded towards positive inifinty. Used in converting coords to indices, which is essentially a binning operation. Parameters ---------- - arr : array_like - Input data: + arr : array_like of float + Data to be rounded. Can be a scalar or array_like of any shape. Returns ------- rounded_array: ndarray - An array or scalar of the same type as `a`, containing the rounded values. + An array or scalar of the same type as ``arr``. """ return np.trunc(np.add(arr, 0.5)) @@ -938,9 +951,9 @@ def dms2rad( Warning ------- - Direction of the angle is determined from `d`, so: + Direction of the angle is determined from ``d``, so: - - if `d` is negative, then `m` & `s` are assumed to be negative. + - if ``d`` is negative, then ``m`` and ``s`` are assumed to be negative. - Negative zero is **not** handled correctly for integer types. This is a limitation of NumPy. diff --git a/src/gsolve/core/xr_accessor.py b/src/gsolve/core/xr_accessor.py index 6c70431..675b095 100644 --- a/src/gsolve/core/xr_accessor.py +++ b/src/gsolve/core/xr_accessor.py @@ -16,7 +16,7 @@ # Copyright (c) 2025 Earth Sciences New Zealand. -"""An accessor class for xarray.DataArray objects.""" +"""An accessor class for ``xarray.DataArray`` objects.""" import numpy as np import xarray as xr @@ -34,7 +34,7 @@ @xr.register_dataarray_accessor("tcorr") class TCorrMethods: """ - A class to providing accessor methods for xarray.DataArray objects. + A class to providing accessor methods for ``xarray.DataArray`` objects. This effectively extends the ``xarray.DataArray`` class by adding the ``tcorr`` accessor, which provides various properties and methods. The accessor @@ -47,17 +47,17 @@ def __init__(self, xarray_dataarray: xr.DataArray) -> None: @property def ydim(self) -> str: - """y dimension name""" + """y dimension name.""" # noqa: D403 return str(self._obj.dims[0]) @property def xdim(self) -> str: - """x dimension name""" + """x dimension name.""" # noqa: D403 return str(self._obj.dims[1]) @property def dx(self) -> float: - """Node separation in x direction""" + """Node separation in x direction.""" x = self.xc if len(x) < 2: return 1.0 # Avoid IndexError for single column @@ -65,7 +65,7 @@ def dx(self) -> float: @property def dy(self) -> float: - """Node separation in y direction""" + """Node separation in y direction.""" y = self.yc if len(y) < 2: return 1.0 # Avoid IndexError for single row @@ -73,17 +73,17 @@ def dy(self) -> float: @property def xc(self) -> np.ndarray: - """x coordinates as a numpy array""" + """X coordinates as a numpy array.""" return self._obj[self.xdim].values @property def yc(self) -> np.ndarray: - """y coordinates as a numpy array""" + """Y coordinates as a numpy array.""" return self._obj[self.ydim].values @property def bounds(self) -> np.ndarray: - """Return array extent as an ndarray of form ``(xmin, ymin, xmax, ymax)``. + """Return array extent as an ndarray of form (xmin, ymin, xmax, ymax). Returns ------- @@ -122,7 +122,7 @@ def clip_to_points( max_dist: float = 0.0, error_if_outside_bounds: bool = True, ) -> xr.DataArray | None: - """Return a copy of this array clipped to the extent of points + max_dist + """Return a copy of this array clipped to the extent of points + max_dist. Parameters ---------- @@ -133,13 +133,13 @@ def clip_to_points( - 2-tuple/sequence of (x, y) arrays or scalars - DataArray : use coordinate arrays as x & y. max_dist : float, default is 0.0 - Extend clip region defined by points by `max_distance` in all directions. + Extend clip region defined by points by ``max_dist`` in all directions. error_if_outside_bounds : bool, default is True If True, raise ValueError if requested clip region lies outside Returns ------- - xr.DataArray + xarray.DataArray The clipped dataarray. """ if isinstance(points, SitesLike): @@ -186,19 +186,18 @@ def clip_to_points( def get_land_sea_mask( self, sea_level_elevation: float = 0.0, name: str = "mask" ) -> xr.DataArray: - """Generate a mask array where "land" areas are True - and "sea" areas are False. + """Generate a mask array where "land" areas are True and "sea" areas are False. Parameters ---------- sea_level_elevation : float, default 0.0 - The theshold elevation + The theshold elevation. name : str, defaut = 'mask' The name of the output DataArray. Returns ------- - mask : xr.DataArray + xarray.DataArray """ da = ( xr.where(self._obj >= sea_level_elevation, 1, 0) @@ -214,7 +213,7 @@ def get_bathymetry_elevation( sea_level_elevation: float = 0.0, name: str = "depth", ) -> xr.DataArray: - """Return an array with all elevations > sea level set to sea level + """Return an array with all elevations > sea level set to sea level. Parameters ---------- @@ -225,6 +224,7 @@ def get_bathymetry_elevation( The threshold elevation. name : str, default is 'depth' The name of the output DataArray. + Returns ------- xr.DataArray @@ -249,26 +249,32 @@ def generate_bathymetry_density( sea_level_elevation: float = 0.0, name: str = "density", ) -> xr.DataArray: - """Generate a bathymetry density grid for computing gravity - corrections for bathymery. + """ + Generate a bathymetry density grid from a boolean mask grid or DEM. + + The output density DataArray can be used in calculating bathymetric terrain + corrections. Bathymety cells are assigned a density of + terrain_density - water_density. Density in topograpjy cells is set to 0.0. Parameters ---------- - land_sea_mask : xr.DataArray | None, default is None - a boolean mask where land is True and sea is False. If None, the mask - will be computed from sea_level_elevation. + land_sea_mask : xarray.DataArray, None, optional + A boolean mask where land/topgraphy is True and sea/bathymetry is + False. If None, a mask will be generated from the calling DataArray + using ``sea_level_elevation`` as the threshold. terrain_density : float, default is 2670.0 - The density of the terrain (kg/m^3). + The density of terrain (kg/m^3). water_density : float, default is 1030.0 - The density of the water (kg/m^3). + The density of water (kg/m^3). sea_level_elevation : float, default is 0.0 - The threshold elevation. + The threshold elevation (m). Ignored if ``land_sea_mask`` is provided. name : str, default is 'density' The name of the output DataArray. Returns ------- - xr.DataArray + xarray.DataArray + Density grid with same dimensions as the calling DataArray. """ land_sea_mask = ( land_sea_mask @@ -320,40 +326,44 @@ def get_topography_elevation( return da def generate_topo_density( - self, terrain_density: float, name: str = "density" + self, terrain_density: float = 2670.0, name: str = "density" ) -> xr.DataArray: - """Generate a terrain density grid for computing gravity corrections - for topography. + """Generate a terrain density grid. + + Output is effectively a copy of the calling DataArray with all values + set to ``terrain_density``. Parameters ---------- - terrain_density : float + terrain_density : float, default is 2670.0 The density of the terrain (kg/m^3). name : str, default is 'density' The name of the output DataArray. Returns ------- - xr.DataArray + xarray.DataArray + A grid with same dimensions as calling DataArray. """ da = xr.full_like(self._obj, terrain_density).drop_attrs().rename(name) return da def clip_to_arr(self, other: xr.DataArray, clip_other: bool = True) -> xr.DataArray: - """Clip `other` DataArray to the dimensions of this DataArray. + """Clip calling DataArray to the dimensions of ``other``, or vice versa. Parameters ---------- - other : xr.DataArray + other : xarray.DataArray The DataArray to be clipped or used as the source for clipping. - clip_other : bool, optional - If True (default), `other` is clipped to the coordinates this object. - If False, then this object will be clipped to the coordinates of `other`. + clip_other : bool, default True + If True, ``other`` is clipped to the dimensions of the calling + DataArray. If False, then the calling DataArray is clipped to + the dimensions of ``other``. Returns ------- - clipped_arr : xr.DataArray - A new DataArray clipped to match the source DataArray. + xarray.DataArray + The clipped DataArray. """ if clip_other: src = self._obj @@ -370,7 +380,19 @@ def clip_to_arr(self, other: xr.DataArray, clip_other: bool = True) -> xr.DataAr ).copy() def is_compatible(self, other: xr.DataArray) -> bool: - """Test that this object and other `other` have the same dimensions and size.""" + """ + Test that caller and ``other`` have same dimensions and size. + + Parameters + ---------- + other : xarray.DataArray + The DataArray to compare with the caller. + + Returns + ------- + bool + True if DataArrays are compatible, False otherwise. + """ if ( self._obj.ndim != other.ndim or self.xdim != other.tcorr.xdim @@ -393,22 +415,24 @@ def generate_distance_mask( Parameters ---------- - point : tuple[float, float] | None, default is None - The (x, y) coordinates of the point to measure distance from. If None, the - center of the array is used. + point : (float, float), optional + The (x, y) coordinates of the point to measure distance from. + If not specified, the centerpoint of the calling DataArray is used. min_dist : float, default is 0.0 - Cells within `min_dist` of `point` are masked out. - max_dist : float | None, default is None - Cells beyond `max_dist` from `point` are masked out. If None, - no maximum distance is applied. + Cells within ``min_dist`` of ``point`` are set to False. + The default 0.0 means no minimum distance is applied. + max_dist : float, optional + If specified, cells beyond ``max_dist`` from ``point`` are + set to False mask_type : {'radial', 'rectangular'}, default is 'radial' - The type of distance mask to generate. 'radial' creates a circular - mask centered on `point`. 'rectangular' creates a square mask. + The type of distance mask to generate. If 'radial', create a + circular mask centered on ``point``. If 'rectangular', create + a square mask centered on ``point``. Returns ------- - mask : np.ndarray - A boolean array where True indicates cells within the specified distance range. + np.ndarray + A boolean array of same dimensions as the calling DataArray. """ if mask_type not in ("radial", "rectangular"): raise ValueError( @@ -451,20 +475,22 @@ def generate_distance_mask( return mask def apply_mask(self, mask: np.ndarray, fill_value: float = 0.0) -> xr.DataArray: - """Apply a boolean mask to this DataArray, filling masked elements - with `fill_value`. + """Mask calling DataArray and set masked elements to ``fill_value``. + + Primarily used to apply a radial or rectangular distance mask to a density + grid before computing terrain corrections. Parameters ---------- - mask : np.ndarray - A boolean array of the same shape as this DataArray. + mask : numpy.ndarray + A boolean array of the same shape as calling DataArray. fill_value : float, default is 0.0 - The value to use for masked elements. + Set masked elements to this value. Returns ------- - masked_arr : xr.DataArray - A new DataArray with the mask applied. + xr.DataArray + A copy of calling DataArray with masked elements set to ``fill_value``. """ return self._obj.where(mask, fill_value) @@ -473,8 +499,8 @@ def cell_edges(self) -> tuple[np.ndarray, np.ndarray]: Returns ------- - x_edges, y_edges : ndarray, ndarray - The x and y coordinate of cell edges. + numpy.ndarray, numpy.ndarray + The x and y coordinates of cell edges. """ x = self.xc y = self.yc diff --git a/src/gsolve/core/xr_methods.py b/src/gsolve/core/xr_methods.py index 96e9977..c139f99 100644 --- a/src/gsolve/core/xr_methods.py +++ b/src/gsolve/core/xr_methods.py @@ -63,7 +63,7 @@ def load_dem( dem_file : str or PathLike A raster grid readable with ``xarray.open_dataset``. input_var_name : str | None, optional - Name of the data variable to read from `dem_file`. Required if dataset + Name of the data variable to read from ``dem_file``. Required if dataset contains multiple variables. output_var_name : str | None, default is "elevation" If not None, name of the data variable in the output DataArray. @@ -119,18 +119,18 @@ def prepare_dem( dem : DataArray or DataSet The Dataset or DataArray to tidy up. input_var_name : str or None, defaut is None - Name of the data variable to extracted from `dem`. Required if `dem` + Name of the data variable to extracted from ``dem``. Required if ``dem`` is a DataSet containing multiple variables. output_var_name : str or None, default None - Rename the DataArray to `output_var_name` if not None. + Rename the DataArray to ``output_var_name`` if not None. x_dim : str, default is "easting" Rename the x-dimension if not ``None``. y_dim : str, default "northing" Rename the y-dimension if not ``None``. fill_nan : float or None, default = 0.0 - If not ``None``, replace null values with `fill_nan`. + If not None, replace null values with ``fill_nan``. round_dp : int or None, default = 3 - If not ``None``, round data to `round_dp` decimal places. + If not None, round data to ``round_dp`` decimal places. Returns ------- @@ -203,7 +203,7 @@ def prepare_dem( def check_dem(dem: xr.DataArray, show: bool = True) -> bool: - """Check `dem` has increasing and evenly spaced coordinates. + """Check ``dem`` has increasing and evenly spaced coordinates. Parameters ---------- @@ -215,7 +215,8 @@ def check_dem(dem: xr.DataArray, show: bool = True) -> bool: Returns ------- bool - True if no issues found, False otherwise.""" + True if no issues found, False otherwise. + """ error_collector = GSolveDataWarning(prefix="check_dem()", show=show) if dem.shape[0] > 1: dff = np.diff(dem.tcorr.yc) @@ -240,14 +241,10 @@ def create_empty_dataarray( x_dim: str = "easting", y_dim: str = "northing", ) -> xr.DataArray: - """Create an empty DataArray with specified coordinates and dimensions. + """Create an empty DataArray with specified dimensions. Parameters ---------- - x_coords : ArrayLike - The coordinates for the x dimension. - y_coords : ArrayLike - The coordinates for the y dimension. var_name : str, default is 'elevation' The name of the variable in the output DataArray. x_dim : str, default is 'easting' @@ -257,8 +254,7 @@ def create_empty_dataarray( Returns ------- - xr.DataArray - An empty DataArray with the specified coordinates and dimensions. + xarray.DataArray """ da = xr.DataArray( [], diff --git a/src/gsolve/gsolve_algorithms.py b/src/gsolve/gsolve_algorithms.py index 0d437c0..c9f96dc 100644 --- a/src/gsolve/gsolve_algorithms.py +++ b/src/gsolve/gsolve_algorithms.py @@ -47,10 +47,10 @@ def call_gsolve_lstsq( ---------- obs : DataFrame The gravity observations to be corrected. The DataFrame must - include columns labeled `['site_id', 'gravity', 'timedelta', 'loop']`. + include columns labeled ``site_id``, ``gravity``, ``timedelta``, and ``loop``. Other columns are ignored. - If `calculate_calibration` is True, then obs must also include a column - labeled `gravity_not_detided`. + If ``calculate_calibration_factor`` is True, then obs must also include a column + labeled 'gravity_not_detided'. ref_sites : DataFrame The reference sites that gravity will be 'tied' to after drift method : {1, 2, 3} @@ -67,9 +67,9 @@ def call_gsolve_lstsq( so if 99.0 is specified, the upper and lower 0.5% of residuals are excluded. use_loops : bool, default True Control how survey loops are treated in the solution. - If ``True``, drift curves are fit to each loop. - If ``False``, a single drift curve is fit to all observations. - solve_for_calibration_factor: bool, default False + If True, drift curves are fit to each loop. + If False, a single drift curve is fit to all observations. + calculate_calibration_factor: bool, default False Calculate gravity meter calibration factor. Returns @@ -147,8 +147,8 @@ def g_solver_lstsq( Loop id for each observation. use_loops : bool Control how survey loops are treated in the solution. - If ``True``, drift curves are fit to each loop. - If ``False``, a single drift curve is fit to all observations. + If True, drift curves are fit to each loop. + If False, a single drift curve is fit to all observations. method : {1, 2, 3} The gsolve solution method to use. Available methods are: diff --git a/src/gsolve/gsolve_outputs.py b/src/gsolve/gsolve_outputs.py index 282cdc9..694cc8a 100644 --- a/src/gsolve/gsolve_outputs.py +++ b/src/gsolve/gsolve_outputs.py @@ -59,7 +59,7 @@ class GSolveSolutionParameters(GSolveParameters): The solution runtime. If not defined, will be set automatically at object initialisation. gsolve_version : str, optional - If unset, attempt to set by calling ``importlib.metadata.version("gsolve")``. + If unset, attempt to set by calling ``importlib.metadata.version()``. """ @@ -100,8 +100,8 @@ class GSolveResults: An object containing the gsolve solution input parameters, runtime metadata, and calculated instrument calibration factor (if applicable). obs_solution : DataFrame - The observation residuals following network adjustment. Observations where ``'active'`` - is False were excluded from the solution after ``percentile_clipping``. + The observation residuals following network adjustment. Observations where the + 'active' field is False were excluded from the solution after 'percentile_clipping'. loop_solution : DataFrame The drift rate and baseline solution for each loop. Columns 'drift' and 'baseline' are the coefficients of a linear equation to compute gravity adjustments @@ -109,9 +109,9 @@ class GSolveResults: .. math:: g_{adj}(t) = g_{drift} t + g_{baseline} - where :math:`t` is time since the start of the ('timedelta' column in - ``obj.obs_solution`` arribute), :math:`g_{drift}` is the drift rate and :math:`g_{baseline}` - is a constant taken from the 'drift' and 'baseline columns of ``loop_solutiom``. + where :math:`t` is time since the start of the (``timedelta`` column in + obj.obs_solution arribute), :math:`g_{drift}` is the drift rate and :math:`g_{baseline}` + is a constant taken from the ``drift`` and ``baseline`` columns of loop_solutiom. Finally, the absolute gravity for some observed gravity :math:`g_{obs}` at time :math:`t` is given by: @@ -132,7 +132,7 @@ class GSolveResults: percentile_clipping: float The percentile clip applied. - """ + """ # noqa: D420 def __init__( self, @@ -388,7 +388,7 @@ def plot_residual_drift( x_col: str = "timedelta" y_col: str = "residual" - drift = float(self.loop_solution.at[loop, "drift"]) # type: ignore[bad-argument-type] + drift = float(self.loop_solution.at[loop, "drift"]) # type: ignore[bad-argument-type] # noqa: PD008 m_loop = self.obs_solution["loop"].eq(loop) m_active = self.obs_solution["active"].eq(True) diff --git a/src/gsolve/meter_conversion.py b/src/gsolve/meter_conversion.py index e6b3118..f9cacc3 100644 --- a/src/gsolve/meter_conversion.py +++ b/src/gsolve/meter_conversion.py @@ -16,6 +16,8 @@ # Copyright (c) 2025 Earth Sciences New Zealand. +"""Module for converting Lacoste-Romberg G and D meter readings to mGal.""" + import warnings from io import StringIO from typing import Any, Protocol, Sequence, TextIO, runtime_checkable @@ -114,7 +116,7 @@ def converter_id(self) -> str: ... class LaCosteRombergDialConverter: """ - Convert Lacoste-Romberg G and D meter readings to mgal. + Convert Lacoste-Romberg G and D meter readings to mGal. Implements table based linear interpolation using the "Calibration Table" provided with each L&R meter. Readings may be filtered by ``meter_id`` and @@ -131,7 +133,7 @@ class LaCosteRombergDialConverter: value_mgal : ArrayLike Gravity in milligals at each ``counter_reading``. interval_factor : ArrayLike, optional - The gradient of mgal/counter_reading for each interval. + The gradient of mGal/counter_reading for each interval. starttime : datetimelike, optional Date from which correction parameters are valid, default is :attr:`pandas.Timestamp.min`. @@ -142,17 +144,17 @@ class LaCosteRombergDialConverter: Attributes ---------- table: DataFrame - The conversion table, with columns 'counter_reading', 'value_mgal', - 'interval_factor' and 'value_mgal_from_ifactor'. + The conversion table, with columns ``counter_reading``, ``value_mgal``, + ``interval_factor`` and ``value_mgal_from_ifactor``. Notes ----- - If `interval_factor` is provided, then 'value_mgal' will be recalculated - and stored in the 'value_mgal_from_ifactor' column. L&R calibration tables - typically provide `value_mgal` rounded to 2 dp (10 ugal resolution) - whereas `interval_factor` is specified to 5 dp (1 ugal resolution). - Corrections are interpolated using 'value_mgal_from_ifactor' where - possible to minimise any loss of precision. + If ``interval_factor`` is provided, then ``value_mgal`` will be recalculated + and stored in the ``value_mgal_from_ifactor`` column. L&R calibration tables + typically provide ``value_mgal`` rounded to 2 dp (10 ugal resolution) + whereas ``interval_factor`` is specified to 5 dp (1 ugal resolution). + Corrections are interpolated using ``value_mgal_from_ifactor`` where + possible to minimise loss of precision. """ _table_header_keys = ("meter_id", "starttime", "endtime") @@ -253,6 +255,7 @@ def __init__( @property def meter_id(self) -> str: + """The ID/serial number of the meter.""" return getattr(self, "_meter_id", "") @meter_id.setter @@ -283,6 +286,13 @@ def set_datetime_range( Date up to which correction parameters are valid. Defaults to None (i.e. no end date). + Raises + ------ + ValueError + If starttime or endtime cannot be converted to a ``pandas.Timestamp``, or if + starttime is >= endtime. + TypeError + If starttime or endtime is not datetimelike, NaT or None. """ if starttime is _pd.NaT or starttime is None: st = None @@ -316,7 +326,7 @@ def set_datetime_range( @property def starttime(self) -> _pd.Timestamp | None: - """The date from which correction parameters are valid.""" + """The date from which correction parameters are valid.""" # noqa: DOC501 st = getattr(self, "_starttime", None) if st is not None and not isinstance(st, _pd.Timestamp): raise TypeError( @@ -326,7 +336,7 @@ def starttime(self) -> _pd.Timestamp | None: @property def endtime(self) -> _pd.Timestamp | None: - """The date up to which correction parameters are valid.""" + """The date up to which correction parameters are valid.""" # noqa: DOC501 r = getattr(self, "_endtime", None) if r is not None and not isinstance(r, _pd.Timestamp): raise TypeError( @@ -348,7 +358,7 @@ def convert_readings( The readings to be converted. meter_id : str, array_like, optional The meter id/name associated with the readings. If provided, only readings - with `meter_id` matching the converter's `meter_id` will be converted. + with ``meter_id`` matching the converter's ``meter_id`` will be converted. date_time : datetimelike, array_like, optional The date/time of the readings. If provided, only readings with ``date_time`` falling within converter's ``valid_date_range`` @@ -358,14 +368,17 @@ def convert_readings( ------- float, ndarray The converted readings. Readings where ``meter_id`` or ``date_time`` - do not match the converter's will be returned as NaN. + do not match the converter's ``meter_id`` or ``valid_date_range`` + will be returned as NaN. Raises ------ ValueError Where reading(s) are outside the limits of the conversion table. + TypeError + If ``meter_id`` is not a string or array of strings, or if ``date_time`` + is not datetimelike or array of datetimelike. """ - interval_bounds: _npt.NDArray[_np.float64] = self.table.index.to_numpy( _np.float64 ) @@ -441,7 +454,14 @@ def convert_readings( return converted def converter_id(self) -> str: - """Identifier label of form 'meter_id:starttime_to_endtime'.""" + """ + Return identifier for this meter conversion table. + + Returns + ------- + str + Identifier label of form 'meter_id:{starttime}_to_endtime'. + """ st = ( "from_" + self.starttime.strftime("%Y-%m-%d") if self.starttime is not None @@ -464,7 +484,8 @@ def from_dataframe( starttime: DatetimeScalar = _pd.Timestamp.min, endtime: DatetimeScalar = _pd.Timestamp.max, ) -> "LaCosteRombergDialConverter": - """Generate a LaCosteRombergDialConverter object from a standard L&R G-meter table. + """ + Generate a LaCosteRombergDialConverter object from a standard L&R G-meter table. The input table data must have at least 3 columns, which are assumed to be "interval_start", "interval_end", "interval_factor". @@ -477,16 +498,15 @@ def from_dataframe( The correction table data. starttime : datetimelike Date from which correction parameters are valid, default is - pandas.Timestamp.min. + ``pandas.Timestamp.min``. endtime : datetimelike Date up to which correction parameters are valid. Defaults to - pandas.Timestamp.max. + ``pandas.Timestamp.max``. Returns ------- - DialToMgalConverter + LaCosteRombergDialConverter """ - return cls( meter_id=meter_id, counter_reading=table["counter_reading"], @@ -504,7 +524,30 @@ def from_dataframe( @classmethod def from_csv(cls, fname: FilePath, **kwargs) -> "LaCosteRombergDialConverter": - """Generate a LaCosteRombergDialConverter object from a csv file.""" + """ + Generate a LaCosteRombergDialConverter object from a csv file. + + Parameters + ---------- + fname : str or TextIO + Path to the csv file, or a file-like object containing the csv data. + kwargs : dict + Additional keyword arguments to be passed to ``pandas.read_csv``. + + Returns + ------- + LaCosteRombergDialConverter + + Raises + ------ + ValueError + Raised if: + + - the file is empty + - required header keys are missing + - column labels are missing or invalid + - readings are outside the limits of the conversion table + """ if isinstance(fname, TextIO): data = fname.readlines() else: diff --git a/src/gsolve/observations.py b/src/gsolve/observations.py index c5135ed..41403f5 100644 --- a/src/gsolve/observations.py +++ b/src/gsolve/observations.py @@ -16,6 +16,8 @@ # Copyright (c) 2025 Earth Sciences New Zealand. +"""Classes for handling gravity observations and pre-processing.""" + import dataclasses import pathlib import warnings as _warnings @@ -76,9 +78,9 @@ class GravityObservationsParameters(GSolveParameters): Attributes ---------- - timedelta_unit : _pd.Timedelta, default "1h" + timedelta_unit : pandas.Timedelta, default "1h" The time interval unit used in calculating survey time deltas. - fixed_time_datum : _pd.Timestamp, default is pd.NaT + fixed_time_datum : pandas.Timestamp, default is pandas.NaT The fixed time datum used for calculating survey time deltas. earthtide_correction_method: str = "" The method used for earth tide correction, e.g. "Longman". @@ -95,7 +97,7 @@ def __setattr__(self, name: str, value: Any) -> None: # noqa: ANN401 if name == "timedelta_unit": value = _pd.Timedelta(value) elif name == "fixed_time_datum": - if value is None or _pd.isnull(value): + if value is None or _pd.isna(value): value = _pd.NaT else: value = to_naive_utc_datetime(value) @@ -104,13 +106,38 @@ def __setattr__(self, name: str, value: Any) -> None: # noqa: ANN401 def to_excel( self, fname: FilePath, - sheet_name: str | None = "observation_parameters", + sheet_name: str = "observation_parameters", if_workbook_exists: IfWorkbookExists = "append", if_sheet_exists: IfSheetExists = "replace", parameter_name_label: str = "parameter", parameter_value_label: str = "value", **kwargs, ) -> None: + """Write parameters to an Excel file. + + Parameters + ---------- + fname : FilePath + The path to the Excel file. + sheet_name : str, default "observation_parameters" + The name of the worksheet to write the parameters to. + if_workbook_exists : {"append", "replace", "error"}, default "append" + What to do if the workbook/file already exists. + if_sheet_exists : {"replace", "error", "new"}, default "replace" + What to do if the worksheet already exists. + parameter_name_label : str, default "parameter" + The label for the parameter names. + parameter_value_label : str, default "value" + The label for the parameter values. + kwargs : Any + Additional keyword arguments passed to ``write_excel_worksheet``. + + See Also + -------- + gsolve.core.excel_io.write_excel_worksheet : for full documentation + of the ``if_workbook_exists`` and ``if_sheet_exists`` parameters. + + """ params_ds = self.to_series( index_name=parameter_name_label, series_name=parameter_value_label ) @@ -123,19 +150,11 @@ def to_excel( ): params_ds["fixed_time_datum"] = "first" - if sheet_name is None: - sheet_name = getattr(self, "_default_excel_sheet_name", None) - if sheet_name is None: - raise ValueError( - "sheet_name is None and object has no " - "_default_excel_sheet_name attribute." - ) - write_excel_worksheet( df=prepare_writable_df( df=params_ds.to_frame(), normalize_column_names=True ), - excel_file=fname, + filename=fname, sheet_name=sheet_name, if_workbook_exists=if_workbook_exists, if_sheet_exists=if_sheet_exists, @@ -147,30 +166,39 @@ class GravityObservations(GSolveTable): """ Class to store and process gravity observations. + The GravityObservations class is effectively a wrapper around a pandas DataFrame, + that provides a mechanism for ensuring data integrity and additional functionality + for handling gravity observations. These methods are sufficient to perform most + tasks, however, users are free to manipulate the data attribute directly as + needed, with the caveat that doing so bypasses data validation and integrity checks. + + In general users will not need to instantiate this class directly, but will instead + use the from_excel() or from_dataframe() class methods to create an instance. + Parameters ---------- - site_id : ArrayLike + site_id : array_like Observation site identifier. - datetime : ArrayLike + datetime : array_like The observation datetime in a format parseable by ``pandas.to_datetime()`` method. All datetimes will be converted to UTC with timezone information removed. - meter_id : ArrayLike + meter_id : array_like Gravity meter identifier. - meter_reading : ArrayLike, optional + meter_reading : array_like, optional Observed meter reading in meter units. At least one of ``meter_reading`` or ``meter_reading_mgal`` must be specified. - meter_reading_mgal : ArrayLike, optional + meter_reading_mgal : array_like, optional Observed meter readings in mGal. At least one of ``meter_reading`` or ``meter_reading_mgal`` must be specified. - obs_id : ArrayLike, optional + obs_id : array_like, optional Array-like object containing unique observation identifiers. If omitted, unique identifiers will be generated from the ``site_id`` and ``datetime`` fields. - loop : ArrayLike, optional + loop : array_like, optional Array-like object containing survey loop identifiers. If omitted, all observations will be assigned to loop '1'. - active : ArrayLike, optional + active : array_like, optional Array-like object indicating whether an observation is 'active' (True) or inactive (False). Only 'active' observations will be included as a datapoints in network adjustment. All observations are considered active by default. @@ -182,24 +210,25 @@ class GravityObservations(GSolveTable): of the earliest observation will be used. **kwargs Additional keyword arguments can be used to specify additional fields to be - included in the ``data`` DataFrame attribute. - . + included in the data DataFrame attribute. If any field defined in ``kwargs`` + match a 'known' field, then the associated ``DataFieldSpecification`` will be used + to validate and coerce the data before it is added to the data DataFrame. Attributes ---------- data : pandas.DataFrame DataFrame containing the gravity observations, gravity reductions and other derived information. - params : GravityObservationsParameters - A container class to store parameters. + A container class to store parameters related to reduction and pre-processing + of gravity observations. _known_fields : dict[str, DataFieldSpecification] - A dictionary of 'known' field name and their associated DataFieldSpecification, - which defines the expected data type, default value, and other metadata for - that field. If data are added using the ``set_column(name, value,...)``, - and 'name' is in ``_known_fields``, the associated DataFieldSpecification + A dictionary of 'known' field names and their associated DataFieldSpecifications, + which define the expected data type, default value, and other metadata for + each field. If data are added using the set_column(name, value, ...), + and 'name' is in _known_fields, the associated ``DataFieldSpecification`` will be used to validate and coerce the data before it is added to the - ``obj.data`` dataframe. + obj.data dataframe. """ @@ -348,7 +377,7 @@ def _index_deduplicator(idx: _pd.Index) -> _pd.Index: Returns ------- Index - A copy of `idx` with 3 digit sequence numbers appended. + A copy of ``idx`` with 3 digit sequence numbers appended. """ suffix = ( idx.to_series().groupby(level=0).cumcount().add(1).astype(str).str.zfill(3) @@ -364,43 +393,44 @@ def set_obs_id( duplicated_obs_id: Literal["error", "keep", "rename"] = "rename", drop: bool = True, ) -> None: - """Set ``obs_id`` as the index of the ``data`` DataFrame attribute. + """Set obs_id as the index of the data DataFrame attribute. - Warning:: This method will overwrite the existing index of ``obj.data``. + Warning:: This method will overwrite the existing index of obj.data. Parameters ---------- idx : ArrayLike, str or None, default is None - The obs_id values to set as the index of ``obj.data``. Behaiviour - depends on the dtype of `idx`. If `idx` is: - - - ``None`` : a default obs_id will be auto-generated - using the ``_default_index_generator()`` method. - - ``str`` : `idx` is assumed to be the name of a column - in ``obj.data`` to be used to set as the index. Equivalent to - ``obj.data.set_index(idx)``. Note that the index will be renamed - to `obs_id`. - - ``array-like`` : `idx` is assumed to be a sequence of obs_id + The obs_id values to set as the index of obj.data. Behaviour + depends on the dtype of ``idx``. If ``idx`` is: + + - None : a default obs_id will be auto-generated + using the _default_index_generator() method. + - str : ``idx`` is assumed to be the name of an existing column + in obj.data to be used to set as the index. Equivalent to + obj.data.set_index(idx). Note that the index will be renamed + to obs_id. + - array-like : ``idx`` is assumed to be a sequence of obs_id values to set as the index. duplicated_obs_id : {'error', 'keep', 'rename'}, default 'rename' The behaviour when duplicate obs_id values are found: - - ``'error'`` : raise a ValueError. - - ``'keep'`` : issue a warning and keep duplicate obs_id's as-is. - - ``'rename'`` : issue a warning and rename duplicate obs_id's by appending + - 'error' : raise a ValueError. + - 'keep' : issue a warning and keep duplicate obs_id's as-is. + - 'rename' : issue a warning and rename duplicate obs_id's by appending a 3-digit sequence number. drop : bool, default True - If ``obs_id`` is to be set from an existing column (i.e. where - ``idx`` is a string), - this flag indicates whether to drop that column from ``obj.data`` after. + If obs_id is to be set from an existing column (i.e. where + ``idx`` is a string), this flag indicates whether to drop that column + from obj.data after. Raises ------ ValueError - + If obs_id contains duplicate values and ``duplicated_obs_id`` is set to 'error'. + TypeError + If ``idx`` is not None, a string or an array-like of obs_id values. """ - if idx is None: # use autogenerated obs_id new_idx = self._default_index_generator() @@ -443,7 +473,17 @@ def set_obs_id( @property def loop_ids(self) -> list[str]: - """Return unique survey loop id's sorted by loop start time.""" + """Return unique survey loop id's sorted by loop start time. + + Returns + ------- + list of str + + Raises + ------ + ValueError + If 'loop' and/or 'datetime' columns are missing from obj.data. + """ if "loop" not in self.data.columns or "datetime" not in self.data.columns: raise ValueError("'loop' and/or 'datetime' columns are missing") loops = self.data["loop"].unique().tolist() @@ -454,33 +494,57 @@ def loop_ids(self) -> list[str]: @property def starttime(self) -> _pd.Timestamp: - """Earlest observation datetime""" + """Earliest observation datetime. + + Returns + ------- + pandas.Timestamp + """ return self.data["datetime"].min() @property def endtime(self) -> _pd.Timestamp: - """Latest observation datetime.""" + """Latest observation datetime. + + Returns + ------- + pandas.Timestamp + """ return self.data["datetime"].max() def timedelta_unit(self) -> _pd.Timedelta: """Time interval unit used for calculating survey timedelta. - Can be any valid argument for `pandas.Timedelta()`. The default + Returns + ------- + pandas.Timedelta + + """ + return self._timedelta_unit + + def set_timedelta_unit( + self, unit: TimedeltaScalar, set_tdelta: bool = True + ) -> None: + """Set time interval unit used for calculating survey timedelta. + + Can be any valid argument for ``pandas.Timedelta()``. The default is '1h' (i.e. 1 hour), meaning survey time is in decimal hours. .. warning:: - Setting a ``time_delta`` unit that is very small in conjunction - with setting a 'distant' ``fixed_time_datum`` will lead to very + Setting a time_delta unit that is very small in conjunction + with setting a 'distant' fixed_time_datum will lead to very large timedelta values being used in gsolve drift calculations. Results may then be incorrect due to floating point rounding errors. - """ - return self._timedelta_unit + Parameters + ---------- + unit : TimedeltaScalar + The time interval unit to use. + set_tdelta : bool, default=True + Whether to update the timedelta values immediately. - def set_timedelta_unit( - self, unit: TimedeltaScalar, set_tdelta: bool = True - ) -> None: + """ self._timedelta_unit = _pd.Timedelta(unit) if set_tdelta: self.set_tdelta() @@ -495,31 +559,41 @@ def fixed_time_datum(self) -> None | _pd.Timestamp: """ return self._fixed_time_datum - def set_fixed_time_datum( - self, t: DatetimeScalar | None, set_tdelta: bool = True - ) -> None: - """Set time datum used for calculating timedelta. + def set_fixed_time_datum(self, t: DatetimeScalar | None, set_tdelta: bool = True) -> None: + """ + Set time datum used for calculating loop and survey timedelta. - By default, gsolve will use the earliest survey and/or loop - observation as the time datum. The `fixed_time_datum` feature is - provided for reproducibility purposes. Legacy gsolve versions used - the J1900.00 epoch as the datum. + Setting a fixed time datum will trigger both "survey_tdelta" and + "loop_tdelta" to be calculated to use the new time datum. The tdelta fields + will then be identical. + + By default, gSolve will use the earliest survey and/or loop + observation as the time datum. The fixed_time_datum feature is + provided for reproducibility purposes. Legacy gSolve versions used + the J1900.00 epoch as the time datum. .. warning:: - Setting a `fixed_time_datum` that is far from the survey - time range in conjuction with setting a small `timedelta_unit` - will lead to very large time_delta values being used in - gsolve drift calculations. Results may then be incorrect due - to floating point rounding errors. + Care should be taken when setting fixed_time_datum. + A fixed_time_datum that is far from the survey time range in conjunction + with setting a small ``timedelta_unit`` will lead to very large time_delta + values being used in gSolve network adjustment calculations. Results may + then be incorrect due to floating point rounding errors. + + For example, using typical Unix Time standards (datum=1970-01-01, unit=sec) + will produce incorrect results. Parameters ---------- - t : _pd.Timestamp or None + t : pandas.Timestamp or None The time datum to use. If None then fixed time datum is - removed. + removed and behaviour will revert to using earliest loop time datum. + Raises + ------ + TypeError + If ``t`` is specified, but is not interpretable as a ``pandas.Timestamp``. """ - if t is None or _pd.isnull(t): + if t is None or _pd.isna(t): self._fixed_time_datum = None else: _t = to_naive_utc_datetime(t) @@ -531,11 +605,18 @@ def set_fixed_time_datum( raise TypeError( f"invalid fixed_time_datum of type '{type(t).__name__}'" ) + if set_tdelta: self.set_tdelta() def params(self) -> GravityObservationsParameters: - """Return parameters as a `GravityObservationsParameters` object.""" + """Return parameters as a ``GravityObservationsParameters`` object. + + Returns + ------- + GravityObservationsParameters + The parameters object. + """ return GravityObservationsParameters( timedelta_unit=self._timedelta_unit, fixed_time_datum=self._fixed_time_datum, @@ -568,7 +649,7 @@ def apply_dial_to_mgal( "datetime" and convert all readings. set_converter_id_column: bool, default=True If True set the "meter_reading_converter_id" column to the - `converter_id` from the `MeterReadingConverter` object. + ``converter_id`` from the ``MeterReadingConverter`` object. input_column_name : str, default='meter_reading' The columns holding gravity readings to convert. output_column_name : str, default='meter_reading_mgal' @@ -606,7 +687,7 @@ def apply_earth_tide_correction( column_name : str, default='earth_tide_corr' The column name to store the earth tide correction. kwargs : dict - Additional keyword arguments passed to the provider's ``tidal_correction()`` + Additional keyword arguments passed to the provider's tidal_correction() method. See Also @@ -619,7 +700,7 @@ def apply_earth_tide_correction( if tide_corrector is None: tide_corrector = LongmanTidalCorrection() - tcorr = tide_corrector.tidal_correction( + et_corr = tide_corrector.tidal_correction( lat=sites.data.loc[ids, "latitude"].to_numpy(), lon=sites.data.loc[ids, "longitude"].to_numpy(), elev=sites.data.loc[ids, "height_ellipsoidal"].to_numpy(), @@ -627,7 +708,7 @@ def apply_earth_tide_correction( site_id=self.data["site_id"], **kwargs, ) - self.set_column(column_name, tcorr) + self.set_column(column_name, et_corr) identifier = tide_corrector.identifier(**kwargs) self._earthtide_correction_method = identifier # self.set_column(f"{column_name}_method", identifier) @@ -639,35 +720,40 @@ def apply_ocean_load_correction( if_not_matched: Literal["error", "warn"] = "error", **kwargs, ) -> None: - """Get ocean loading corrections and store in column `column_name`. + """ + Get ocean loading corrections and store in column ``column_name``. - This method calls the ``ocean_load_correction()`` method of the provided - `ocean_load_corrector` object to retrieve ocean loading corrections for + This method calls the ocean_load_correction() method of the provided + ``corrector`` object to retrieve ocean loading corrections for each observation. Ocean load corrections will typically have been pre-computed in some Third Party software such as Quick Tide Pro. Parameters ---------- - ocean_load_corrector : OceanLoadCorrectionProvider + corrector : OceanLoadCorrectionProvider An object providing ocean loading corrections. sites : GravitySites, optional GravitySites object providing latitude, longitude and height. Only required - if the `ocean_load_corrector` requires site location parameters. + if the ``corrector`` requires site location parameters. column_name : str, default='ocean_load_corr' The column name to store the ocean loading correction. if_not_matched : {'error', 'warn'}, default 'error' Behaviour when an observation cannot be matched with the corrections - provided by the `ocean_load_corrector`. E.G. for the timeseries based - corrector `QuickTideTimeSeries`, if datetimes are outside the range of + provided by the ``corrector``. E.g. for the timeseries based + corrector ``QuickTideTimeSeries``, if datetimes are outside the range of the time series. Options are: - 'error' : raise a ValueError. - - 'warn' : issue a warning, and return nan for unmatched observations. + - 'warn' : issue a warning, and set unmatched observations to NaN. kwargs : dict Additional keyword arguments passed to the provider's - ``ocean_load_correction()`` method. + ocean_load_correction() method. + Raises + ------ + TypeError + If ``corrector`` does not implement the ``OceanLoadCorrectionProvider`` protocol. """ if not isinstance(corrector, OceanLoadCorrectionProvider): raise TypeError( @@ -680,7 +766,7 @@ def apply_ocean_load_correction( if_not_matched=if_not_matched, **kwargs, ) - self.set_column(column_name, corrections) + self.set_column(label=column_name, data=corrections) self._ocean_load_correction_method = corrector.identifier() def set_calibration_factor( @@ -694,23 +780,23 @@ def set_calibration_factor( Parameters ---------- calibration_factor : float or array_like - The gravity meter `calculate_calibration`. Default is 1.0 + The gravity meter calibration factor. Default is 1.0. meter_id : str, default None - Set `calibration_factor` for specifed `meter_id` only. If data - contains multiple gravity meters, `meter_id` must be specified. + Set ``calibration_factor`` for specified ``meter_id`` only. If data + contains multiple gravity meters, ``meter_id`` must be specified. Raises ------ ValueError - If data contains multiple gravity meter_id's and `meter_id` is None. - When specified `meter_id` is not in data. + If data contains multiple gravity meter_id's and ``meter_id`` is None. + When specified ``meter_id`` is not in data. """ c_label: str = "calibration_factor" if self.data["meter_id"].nunique() > 1 and meter_id is None: raise ValueError( - "Multiple gravity meters found in data, must specify `meter_id`" + "Multiple gravity meters found in data, must specify ``meter_id``" ) if meter_id is None: self.set_column(c_label, float(calibration_factor)) @@ -726,7 +812,7 @@ def set_calibration_factor( self.data.loc[this_meter, c_label] = float(calibration_factor) def calculate_tide_corrected_gravity(self) -> None: - """Calculate corrected gravity values and assign to column 'gravity_corr'""" + """Calculate corrected gravity values and assign to column 'gravity_corr'.""" reading = self.data["meter_reading_mgal"] calibration_factor = self.data.get("calibration_factor", 1.0) etide_corr = self.data.get("earth_tide_corr", 0.0) @@ -737,13 +823,21 @@ def calculate_tide_corrected_gravity(self) -> None: self.set_column("gravity_corr", gcorr) def set_tdelta(self) -> None: - """Calculate time delta for survey and loop observations and - assign to columns "survey_tdelta","loop_tdelta". + """Calculate time delta for survey and loop observations. - The datum for survey_tdelta is the earliest observation (i.e self.starttime), - while the datum for loop_tdelta is the earliest observation in each loop. + Columns "survey_tdelta" and "loop_tdelta" are added and/or updated. + The default "fixed_time_datum" is None, meaning that for: - The default datum(s) can be overridden by calling `set_fixed_time_datum()` + - "survey_tdelta" the datum is the earliest observation (i.e self.starttime), + - "loop_tdelta" the datum is the earliest observation in each loop. + + The default can be overridden by calling set_fixed_time_datum(), in which case + both "survey_tdelta" and "loop_tdelta" will use the same fixed_time_datum. + + See Also + -------- + fixed_time_datum : Get the fixed time datum used for timedelta calculations. + set_fixed_time_datum : Set the fixed time datum used for timedelta calculations. """ datetime_col = "datetime" @@ -783,40 +877,29 @@ def activate( obs_id: str | Iterable[str] | None = None, site_id: str | Iterable[str] | None = None, loop: str | Iterable[str] | None = None, - add_metadata: bool = False, ) -> None: - """Activate observations. + """ + Set the "active" flag/field to True for specified observations. - Only 'active' observations are included in gsolve solutions. - By default, all observations are considered 'active'. This method - allows for the reactivation of observations that were specified - as inactive in the input data or by calling the `deactivate` - method. + The "active" flag controls which observations are included in a + gsolve network adjustment. Observations are set as + active==True, unless explicitly. This method allows for the reactivation + of observations that were set as inactive in the input data or by calling the + deactivate method. Parameters ---------- obs_id : str or array_like, optional - The `obs_id` of the observations to activate. + The ``obs_id`` of the observations to activate. site_id : str or array_like, optional - The `site_id` of the observations to activate. + The ``site_id`` of the observations to activate. loop : str or array_like, optional - The `loop` of the observations to activate. - add_metadata : bool, default=False - Not implemented. - - Raises - ------ - Value Error : - If any of the specified `obs_id`, `site_id` or `loop` are not - in the data. + The ``loop`` of the observations to activate. See Also -------- deactivate : equivalent function to deactivate observations. """ - if add_metadata: - raise NotImplementedError("add_metadata not implemented") - self._activate_deactivate(True, obs_id, site_id, loop) def deactivate( @@ -824,29 +907,25 @@ def deactivate( obs_id: str | Iterable[str] | None = None, site_id: str | Iterable[str] | None = None, loop: str | Iterable[str] | None = None, - add_metadata: bool = False, ) -> None: - """Deactivate observations. + """ + Set the "active" flag/field to False for specified observations. Deactivated observations are not included in gsolve solutions. Parameters ---------- obs_id : str or array_like, optional - The `obs_id` of the observations to deactivate. + The ``obs_id`` of the observations to deactivate. site_id : str or array_like, optional - The `site_id` of the observations to deactivate. + The ``site_id`` of the observations to deactivate. loop : str or array_like, optional - The `loop` of the observations to deactivate. - add_metadata : bool, default=False - Not implemented. + The ``loop`` of the observations to deactivate. See Also -------- activate : Activate observations """ - if add_metadata: - raise NotImplementedError("add_metadata not implemented") self._activate_deactivate(False, obs_id, site_id, loop) def _activate_deactivate( @@ -856,15 +935,30 @@ def _activate_deactivate( site_id: str | Iterable[str] | None = None, loop: str | Iterable[str] | None = None, ) -> None: - """Activate or deactivate observations. + """ + Activate or deactivate observations. Use the activate and deactivate methods rather than calling this method directly. - See Also - -------- - activate : Activate observations - deactivate: Deactivate observations + Parameters + ---------- + flag : bool + The value to set the "active" field to. True for activate, + False for deactivate. + obs_id : str or array_like, optional + The ``obs_id`` of the observations to deactivate. + site_id : str or array_like, optional + The ``site_id`` of the observations to deactivate. + loop : str or array_like, optional + The ``loop`` of the observations to deactivate. + + Raises + ------ + ValueError + If any specified ``obs_id``, ``site_id`` or ``loop`` values are not found in the data. + TypeError + If any input is not a string or iterable of strings. """ def _parse_inputs(o: str | Iterable[str] | None) -> list[str]: @@ -877,9 +971,11 @@ def _parse_inputs(o: str | Iterable[str] | None) -> list[str]: else: raise TypeError(f"invalid input of type '{type(o).__name__}'") + # parse all args first to check for errors before modifying data _obs_id = _parse_inputs(obs_id) _site_id = _parse_inputs(site_id) _loop = _parse_inputs(loop) + if _obs_id: missing = [oi for oi in _obs_id if oi not in self.data.index.to_list()] if missing: @@ -913,7 +1009,7 @@ def _get_writable_df( include_unknown_fields: bool | Sequence[str] = False, active_only: bool = False, ) -> _pd.DataFrame: - """Return a DataFrame suitable for writing to a file.""" + """Return a DataFrame suitable for writing to an excel or csv file.""" # noqa: DOC201 cols = [c for c in self.known_fields() if c in self.data.columns] if include_unknown_fields is not False: if include_unknown_fields is True: @@ -998,7 +1094,7 @@ def to_excel( include_unknown_fields=include_unknown_fields, active_only=active_only, ), - excel_file=fname, + filename=fname, sheet_name=sheet_name, if_workbook_exists=if_workbook_exists, if_sheet_exists=if_sheet_exists, @@ -1019,6 +1115,7 @@ def plot_observed_data( savefilename: FilePath | None = None, figsize: tuple[float, float] = (12, 8), ax=None, # noqa: ANN001 + show: bool = True, **kwargs, ) -> tuple[_plt.Figure, _plt.Axes]: """ @@ -1027,23 +1124,32 @@ def plot_observed_data( Parameters ---------- loop : str or int - The loop to plot. Use `loop='all'` to plot all data, ignoring loops. + The loop to plot. Use ``loop='all'`` to plot all data, ignoring loops. x_column : str, default='datetime' The 'x' data column to plot. y_column : str, default='meter_reading_mgal' The 'y' data column to plot. savefilename : FilePath, default=None - If not None, save the plot to `savefilename`. + If not None, save the plot to ``savefilename``. figsize : tuple, default=(12, 8) The pyplot figure size. - ax = None - **kwargs : dict - Optional keyword arguments passed directly to `matplotlib.pyplot.plot()`. - + ax : matplotlib.axes.Axes, optional + If provided, the plot will be drawn on this axes. Otherwise, a new figure + and axes will be created. + show : bool, default=True + Whether to call fig.show() to display the plot. If False, the figure + will be created but not displayed. This can be useful when calling this method + from a non-interactive environment, or when further modifying the figure before + displaying or saving it. + kwargs : dict + Optional keyword arguments passed directly to matplotlib.pyplot.plot(). Returns ------- - Plot of the observed data. + fig : matplotlib.figure.Figure + The figure object containing the plot. + ax : matplotlib.axes.Axes + The axes object in which the plot is drawn. """ if loop == "all": y_data = self.data.loc[:, y_column] @@ -1080,10 +1186,12 @@ def plot_observed_data( fout = fout.parent / f"{fout.stem}_loop_{loop}{fout.suffix}" _plt.savefig(fout, dpi=300) - fig.show() + if show: + fig.show() return fig, ax def _make_network(self, sites: GravitySites) -> _pd.DataFrame: + # TODO: Fix me? use a copy of self.data to avoid modifying the original DataFrame self.data["group"] = ( self.data["site_id"] != self.data["site_id"].shift() ).cumsum() @@ -1104,32 +1212,41 @@ def plot_network_map( marker_scale_factor: float = 25, plot_stn_labels: bool = False, ax: _plt.Axes | None = None, + show: bool = True, **kwargs, ) -> tuple[_plt.Figure, _plt.Axes]: """ Plot network map showing connections between stations. - Station markers are scaled according to the number of occupations + + Station markers are scaled according to the number of occupations. Parameters ---------- sites : GravitySites - DESCRIPTION. + Object providing site location data.. savefilename : str or PathLike, optional - DESCRIPTION. The default is None. + If specified, save the figure to file. figsize : tuple, default=(10, 10) The pyplot figure size. marker_scale_factor: float, default=25 Scale marker size by this value. plot_stn_labels: bool, default=False Plot station name next to station points. - ax=None - **kwargs : dict - Optional keyword arguments passed directly to `matplotlib.pyplot.plot()`. + ax : matplotlib.axes.Axes, optional + If provided, the plot will be drawn on this axes. Otherwise, a new figure + and axes will be created. + show : bool, default=True + Whether to call fig.show() to display the plot. If False, the figure + will be created but not displayed. + kwargs : dict + Optional keyword arguments passed directly to matplotlib.pyplot.plot(). Returns ------- - A figure showing connections between sites as well as a tuple containing the - figure and axis objects (fig, ax). + fig : matplotlib.figure.Figure + The figure object containing the plot. + ax : matplotlib.axes.Axes + The axes object in which the plot is drawn. """ if "marker" not in kwargs: @@ -1171,7 +1288,8 @@ def plot_network_map( fout = fout.parent / f"{fout.stem}{fout.suffix}" _plt.savefig(fout, dpi=300) - _ = fig.show() + if show: + _ = fig.show() return fig, ax @@ -1184,10 +1302,6 @@ def plot_site_visits(self, loop: str) -> None: loop : str Loop number to plot. - Returns - ------- - A figure showing the station occupation order for each loop. - """ df = self.data.loc[self.data["loop"].eq(loop)] @@ -1200,7 +1314,7 @@ def plot_site_visits(self, loop: str) -> None: ) def loop_summary(self) -> _pd.DataFrame: - """Return a summary of the observations by loop.""" + """Return a summary of the observations by loop.""" # noqa: DOC201 from gsolve.core._summary_functions import ( duration_hr, endtime_utc, @@ -1215,7 +1329,19 @@ def loop_summary(self) -> _pd.DataFrame: return self.data.groupby("loop").agg(agg_dict).droplevel(0, axis=1) def site_summary(self, data_col: str | None = None) -> _pd.DataFrame: - """Return a summary of the observations by site.""" + """Return summary statistics of observations by site. + + Parameters + ---------- + data_col : str | None, optional + The data column to summarize. If None, then use gravity_corr if + this column has been set else fallback to meter_reading_mgal. + + Returns + ------- + DataFrame + Summary statistics of observations by site. + """ from gsolve.core._summary_functions import ( in_loops, n, @@ -1227,12 +1353,21 @@ def site_summary(self, data_col: str | None = None) -> _pd.DataFrame: agg_dict: dict[str, Any] = {"active": [n, n_inactive]} if data_col is None: - if self.data["gravity_corr"].notna().all(): + if ( + "gravity_corr" in self.data.columns + and self.data["gravity_corr"].notna().all() + ): data_col = "gravity_corr" - elif self.data["meter_reading_mgal"].notna().all(): + elif ( + "meter_reading_mgal" in self.data.columns + and self.data["meter_reading_mgal"].notna().all() + ): data_col = "meter_reading_mgal" else: - data_col = "meter_reading" + raise ValueError( + "Cannot compute summary statistics: Columns 'gravity_corr' " + "and/or 'meter_reading_mgal' are missing or contain null values." + ) if data_col not in self.data.columns: raise ValueError(f"Data column '{data_col}' not found in data") @@ -1310,7 +1445,7 @@ def check_data(self, warn: bool = True) -> bool: class GravitySurvey: - """Class to store gravity observations and sites and facilitate running gsolve + """Class to store gravity observations and sites and facilitate running gSolve. Parameters ---------- @@ -1318,7 +1453,7 @@ class GravitySurvey: The gravity observations object. sites : GravitySites The gravity sites object. - . + """ def __init__(self, obs: GravityObservations, sites: GravitySites) -> None: @@ -1345,6 +1480,11 @@ def from_excel( Ignore unknown fields in the excel file. parse_split_datetime : bool, default is True Parse split datetime fields into a single datetime column. + + Returns + ------- + GravitySurvey + The gravity survey object. """ obs = GravityObservations.from_excel( fname, @@ -1373,7 +1513,7 @@ def apply_dial_to_mgal( Parameters ---------- converter : MeterReadingConverter, optional - The dial to mgal converter object. If None, `meter_reading` + The dial to mgal converter object. If None, meter_reading data are assumed to be in mgal and no conversion is applied. input_column_name : str, default='meter_reading' The input column name to convert. @@ -1389,6 +1529,7 @@ def apply_dial_to_mgal( def apply_earth_tide_correction( self, tide_corrector: EarthTideCorrectionProvider, **kwargs ) -> None: + """Apply earth tide correction to observations.""" self.observations.apply_earth_tide_correction( self.sites, tide_corrector, **kwargs ) @@ -1396,9 +1537,11 @@ def apply_earth_tide_correction( def set_calibration_factor( self, calibration_factor: float = 1.0, meter_id: str | None = None ) -> None: + """Set gravity meter calibration factor on observations.""" self.observations.set_calibration_factor(calibration_factor, meter_id=meter_id) def calculate_tide_corrected_gravity(self) -> None: + """Calculate tide-corrected gravity for observations.""" self.observations.calculate_tide_corrected_gravity() def set_reference_gravity( @@ -1406,6 +1549,7 @@ def set_reference_gravity( ref_grav: ReferenceGravity | _pd.DataFrame, reset: bool = False, ) -> None: + """Set reference gravity values for sites.""" self.sites.set_reference_gravity(ref_grav, reset) # def summary(self, fmt: str = "dict") -> dict | _pd.DataFrame: @@ -1413,12 +1557,17 @@ def set_reference_gravity( # return self.observations.summary() def pre_flight_check(self, warn: bool = True) -> bool: - """Check data are valid before running gsolve. + """Check data are valid before performing network adjustment. Parameters ---------- warn : bool, default=True - If True, print warnings. + If True, print a warning for each issue. If False, only return True/False. + + Returns + ------- + bool + True if data are valid, False otherwise. """ rval = True if not self.observations.check_data(warn=warn): @@ -1445,6 +1594,26 @@ def solve_lstsq( use_loops: bool = True, calculate_calibration_factor: bool = False, ) -> GSolveResults: + """Perform network adjustment on gravity observations. + + Parameters + ---------- + method : GSolveSolverMethod + The solver method to use for the least squares adjustment. + percentile_clipping : float, default=100 + The percentile of residuals to use for clipping. Values outside this + percentile will be excluded from the adjustment. Must be between 0 and 100. + use_loops : bool, default=True + If True, compute drift on a loop-by-loop basis. If False, compute a single + drift adjustmen for all data. + calculate_calibration_factor : bool, default=False + If True, calculate the calibration factor during the adjustment. + + Returns + ------- + GSolveResults + The results of the network adjustment. + """ self.observations.set_tdelta() td_column = "loop_tdelta" if use_loops else "survey_tdelta" @@ -1475,36 +1644,36 @@ def combine_gravity_observations( ) -> GravityObservations: """Merge 2 or more GravityObservations objects. - The returned object is formed by concatenating the `data` DataFrames attributes - of each `obs` object, and then instantiating a new GravityObservations object. - Non `data` attributes of the new object (e.g. `timedelta_unit`) are set - from the the first `obs` specified. + The returned object is formed by concatenating the data DataFrame attributes + of each ``obs`` object, and then instantiating a new ``GravityObservations`` object. + Non-data attributes of the new object (e.g. timedelta_unit) are set + from the the first ``obs`` specified. Parameters ---------- obs : GravityObservations - Sequence of two or more `GravityObservations` objects to be combined. + Sequence of two or more ``GravityObservations`` objects to be combined. duplicated_loops : {'error', 'keep', 'drop', 'rename'}, defaut is 'error' How to handle situations where ``loop`` identifiers are duplicated between GravityObservations objects: - - ``'error'`` : raise a ValueError - - ``'keep'`` : duplicates are unchanged - - ``'drop'`` : drop all data with duplicated loop_id - - ``'rename'`` : rename the duplicate loops by adding suffix - ``_merged_{int}`` where ``{int}`` refers to the position in - the input ``obs`` array. + - 'error' : raise a ValueError + - 'keep' : duplicates are unchanged + - 'drop' : drop all data with duplicated loop_id + - 'rename' : rename the duplicate loops by adding suffix + _merged_{int} where {int} refers to the position in + the input ``obs`` array. - duplicated_obs_id : {'error', 'drop', 'rename'}, default is 'error' + duplicated_obs_ids : {'error', 'drop', 'rename', 'regenerate'}, default is 'error' How to handle situations where ``obs_id`` identifiers are duplicated between GravityObservations objects: - - ``'error'`` : raise a ValueError - - ``'drop'`` : drop data with duplicated ``obs_id``. - - ``'rename'`` : rename the duplicate obs_id's by adding suffix - ``_merged_{int}``` where ``{int}`` refers to the position in - the input ``obs`` array. - - ``'regenerate'`` : generate new obs_id's for all data in the + - 'error' : raise a ValueError + - 'drop' : drop data with duplicated ``obs_id``. + - 'rename' : rename the duplicate obs_id's by adding suffix + _merged_{int} where {int} refers to the position in + the input ``obs`` array. + - 'regenerate' : generate new obs_id's for all data in the merged object. Returns @@ -1519,15 +1688,17 @@ def combine_gravity_observations( if not all([isinstance(o, GravityObservations) for o in obs]): raise TypeError(f"invalid type for elements in obs") - if duplicated_loops not in ("error", "keep", "drop", "rename"): + duplicated_loops_options = ("error", "keep", "drop", "rename") + if duplicated_loops not in duplicated_loops_options: raise ValueError( f"invalid duplicated_loops arg '{duplicated_loops}', " - "must be one of 'error', 'keep', 'drop', 'rename'" + f"must be one of {duplicated_loops_options}" ) - if duplicated_obs_ids not in ("error", "drop", "rename"): + duplicated_obs_ids_options = ("error", "drop", "rename", "regenerate") + if duplicated_obs_ids not in duplicated_obs_ids_options: raise ValueError( f"invalid duplicated_obs_id arg {duplicated_obs_ids}," - "must be one of 'error', 'drop', 'rename'" + f"must be one of {duplicated_obs_ids_options}" ) target = obs.pop(0) @@ -1620,32 +1791,46 @@ def combine_gravity_surveys( ) -> GravitySurvey: """Merge 2 or more GravitySurveys objects. - The returned object is formed by concatenating the `observations` DataFrames - attributes of each object in `survey`, and then instantiating a new GravitySurvey - object. Non `observations` attributes of the new object (e.g. `timedelta_unit`) are - set from the the first `survey` specified. + The returned object is formed by concatenating the observations and sites DataFrame + attributes of each object in ``surveys``, and then instantiating a new + ``GravitySurvey`` object. Non-observation attributes of the new object + (e.g. timedelta_unit) are set from the the first ``surveys[0]``. Parameters ---------- surveys : GravitySurveys - A list of two or more `GravitySurveys` objects to be combined. - ignore_duplicates : bool, default is False - Specify how data with duplicate index and/or loop values are to be handled. - If True, duplicates will be merged unchanged. If False, raise a ValueError. + A list of two or more ``GravitySurveys`` objects to be combined. + duplicated_loops : {'error', 'keep', 'drop', 'rename'}, defaut is 'error' + How to handle situations where ``loop`` identifiers are duplicated between + observations of different ``GravitySurveys`` objects: + - 'error' : raise a ValueError + - 'keep' : duplicates are unchanged + - 'drop' : drop all data with duplicated loop_id + - 'rename' : rename the duplicate loops by adding suffix + _merged_{int} where {int} refers to the position in + the input ``surveys`` array. + + duplicated_obs_ids : {'error', 'drop', 'rename', 'regenerate'}, default is 'error' + How to handle situations where ``obs_id`` identifiers are duplicated between + observations of different ``GravitySurveys`` objects: + - 'error' : raise a ValueError + - 'drop' : drop data with duplicated ``obs_id``. + - 'rename' : rename the duplicate obs_id's by adding suffix + _merged_{int} where {int} refers to the position in + the input ``surveys`` array. + - 'regenerate' : generate new obs_id's for all data in the + merged object. + + duplicated_sites : {'error', 'drop'}, default is 'error' + How to handle situations where ``site_id`` identifiers are duplicated between + sites of different ``GravitySurveys`` objects: + - 'error' : raise a ValueError + - 'drop' : drop data with duplicated ``site_id``. Returns ------- GravitySurvey The new GravitySurvey object. - - Raises - ------ - ValueError - If less than 2 `GravitySurveys` are specified. - If duplicate `obs_id` or `loop` values are found and ignore_duplicates=False. - TypeError - If any of `*surveys` is not a GravitySurvey object. - """ if len(surveys) < 2: raise ValueError("Must specify at least 2 GravitySurveys objects.") diff --git a/src/gsolve/reductions/anomalies.py b/src/gsolve/reductions/anomalies.py index f0346ad..2547b4f 100644 --- a/src/gsolve/reductions/anomalies.py +++ b/src/gsolve/reductions/anomalies.py @@ -29,7 +29,7 @@ GravityCorrectionProvider, GravityCorrections, ) -from gsolve.reductions.terrain_corrections import TerrainCorrectionData +from gsolve.reductions.terrain_corrections import TerrainCorrectionData, TerrainCorrectionParameters from gsolve.sites import GravitySites __all__ = [ @@ -57,8 +57,8 @@ def compute_complete_bouguer_anomaly( atmospheric_correction: ArrayLike = 0.0, spherical_bouguer_cap_correction: ArrayLike = 0.0, ) -> _np.ndarray: - r""" - Calculate the complete Bouguer anomaly. + """ + Calculate the Complete Bouguer anomaly. The complete Bouguer anomaly is calculated using the following formula: @@ -78,25 +78,29 @@ def compute_complete_bouguer_anomaly( Parameters ---------- - absolute_gravity : ArrayLike + absolute_gravity : array-like Observed absolute gravity. - normal_gravity : ArrayLike + normal_gravity : array-like Normal gravity value at the ellipsoid. - free_air_correction : ArrayLike - bouguer_correction : ArrayLike + free_air_correction : array-like + The free air correction. + bouguer_correction : array-like Bouguer correction for infinite planar slab or for curvature corrected. If curvature corrected, ensure ``spherical_bouguer_cap_correction`` = 0.0. - terrain_correction : ArrayLike - atmospheric_correction : ArrayLike, default = 0.0 - spherical_bouguer_cap_correction : ArrayLike, default = 0.0 + terrain_correction : array-like + atmospheric_correction : array-like, default = 0.0 + spherical_bouguer_cap_correction : array-like, default = 0.0 Returns ------- - complete_bouguer_anomaly : _np.ndarray + numpy.ndarray The complete Bouguer anomaly in mGal. + See Also + -------- + compute_simple_bouguer_anomaly : Calculate the Simple Bouguer anomaly. + compute_free_air_anomaly : Calculate the Free Air anomaly. """ - if any( _args_contain_nulls( absolute_gravity, @@ -131,9 +135,11 @@ def compute_simple_bouguer_anomaly( atmospheric_correction: ArrayLike = 0.0, spherical_bouguer_cap_correction: ArrayLike = 0.0, ) -> _np.ndarray: - """Calculate the simple Bouguer anomaly; terrain corrections not included + """Calculate the Simple Bouguer anomaly from provided corrections. - The simple Bouguer anomaly is calculated using the following formula: + The Simple Bouguer anomaly differs from the Complete Bouguer anomaly + in that terrain corrections are not included. It is calculated using + the following formula: .. math:: SBA = AG - (NG + FAC + AC + BSC + SBC) @@ -149,26 +155,30 @@ def compute_simple_bouguer_anomaly( Parameters ---------- - absolute_gravity : ArrayLike + absolute_gravity : array-like Observed absolute gravity in mGal. - normal_gravity : ArrayLike + normal_gravity : array-like Normal gravity value at the ellipsoid in mGal. - free_air_correction : ArrayLike + free_air_correction : array-like The free air correction in mGal. - bouguer_correction : ArrayLike + bouguer_correction : array-like Bouguer correction for infinite planar slab or for curvature corrected in mGal. If curvature corrected, ensure ``spherical_bouguer_cap_correction`` = 0.0. - atmospheric_correction : ArrayLike, default = 0.0 + atmospheric_correction : array-like, default = 0.0 The atmospheric correction in mGal. - spherical_bouguer_cap_correction : ArrayLike, default = 0.0 + spherical_bouguer_cap_correction : array-like, default = 0.0 The spherical Bouguer cap correction in mGal. Should be zero if ``bouguer_correction`` is curvature corrected. Returns ------- - simple_bouguer_anomaly : array or DataFrame - Bouguer anomaly in mGal without terrain correction. + numpy.ndarray + The simple Bouguer anomaly in mGal. + See Also + -------- + compute_complete_bouguer_anomaly : Calculate the Complete Bouguer anomaly. + compute_free_air_anomaly : Calculate the Free Air anomaly. """ if any( _args_contain_nulls( @@ -249,41 +259,41 @@ class GravityAnomalies(GSolveTable): An object providing site_id's and associated absolute gravity values for which anomalies will be computed. Can be any of the following: - - ``GSolveResults`` : the output of a gsolve network adjustment. - - ``DataFrame`` : must contain an ``'absolute_gravity'`` column and be indexed by - ``'site_id'`` - - ``Series`` : absolute gravity values indexed by ``'site_id'``. + - GSolveResults : the output of a gsolve network adjustment. + - DataFrame : must contain an 'absolute_gravity' column and be indexed by + 'site_id' + - Series : absolute gravity values indexed by 'site_id'. sites : GravitySites, GravitySurvey or DataFrame An object providing the geographic coordiates and ellipsoidal height for each site. Can be any of the following: - - ``GravitySites`` or ``GravitySurvey`` : A gsolve object providing site metadata. - - ``DataFrame`` : must contain columns ``'latitude'``, ``'longitude'`` and - ``'height_ellipsoidal'`` and be indexed by ``'site_id'``. + - GravitySites or GravitySurvey : A gsolve object providing site metadata. + - DataFrame : must contain columns 'latitude', 'longitude' and + 'height_ellipsoidal' and be indexed by 'site_id'. corrections_parameters : GravityCorrectionParameters, GravityCorrectionProvider or GravityCorrections An object providing either the parameters used to compute the various gravity corrections and/or a set of pre-computed gravity corrections. Can be any of the following: - - ``GravityCorrectionParameters`` : a parameter object defining + - GravityCorrectionParameters : a parameter object defining how to compute gravity corrections. The parameters object will be copied - to ``self.params`` attribute. - - ``GravityCorrectionProvider`` : a class for computing gravity corrections + to self.params attribute. + - GravityCorrectionProvider : a class for computing gravity corrections as specified in a GravityCorrectionParameters object. This will be used directly to compute the necessary gravity corrections, and - it's `params` copied to ``self.params``. - - ``GravityCorrections`` : pre-computed gravity corrections for a set of sites + its ``params`` copied to self.params. + - GravityCorrections : pre-computed gravity corrections for a set of sites according to parameters in a GravityCorrectionParameters object. The - corrections used dircetly and , and it's `params` copied to ``self.params`` + corrections used dircetly and , and its ``params`` copied to self.params terrain_corrections : TerrainCorrectionData, optional An object providing terrain corrections at each site. These are required to compute the complete Bouguer anomaly. If provided, georgraphic coordinates and - terrain corrections will be copied to ``self.data`` and the associated - TerrainCorrectionParameter objects copied to ``self.tcorr_params``. If None, - then a terrain correction column ``'tcorr:total'`` will be added and set to NaN. + terrain corrections will be copied to self.data and the associated + TerrainCorrectionParameter objects copied to self.tcorr_params. If None, + then a terrain correction column 'tcorr:total' will be added and set to NaN. Attributes ---------- @@ -291,36 +301,36 @@ class GravityAnomalies(GSolveTable): Table of computed gravity corrections and anomalies indexed by ``site_id``. The primary columns are: - - ``absolute_gravity`` : the input absolute gravity values. - - ``normal_gravity_at_ellipsoid`` : normal gravity at surface of the ellipsoid - ``self.params.ellipsoid`` - - ``free_air_correction`` : the free-air correction. - - ``atmospheric_correction`` : the atmospheric corrections due to elevation. + - absolute_gravity : the input absolute gravity values. + - normal_gravity_at_ellipsoid : normal gravity at surface of the ellipsoid + self.params.ellipsoid + - free_air_correction : the free-air correction. + - atmospheric_correction : the atmospheric corrections due to elevation. Only inclued if ``self.params.use_atmospheric_correction`` is True. - - ``bouguer_slab_correction`` or ``bouguer_slab_curvature_corrected`` : the + - bouguer_slab_correction or bouguer_slab_curvature_corrected : the Bouguer correction, with form determined by ``self.params.use_curvature_corrected``. - - ``tcorr:*`` : terrain correction for various zones, if terrain corrections + - tcorr:* : terrain correction for various zones, if terrain corrections were provided. Note that only the ``tcorr:total`` column is used in anomaly calculations. - - ``tcorr:total`` : sum of contributions from each terrain correction zone. Will + - tcorr:total : sum of contributions from each terrain correction zone. Will be NaN if no terrain corrections were provided. - - ``free_air_anomaly`` : the free-air anomaly in mGal. - - ``bouguer_anomaly_simple`` : the Bouguer anomaly without terrain corrections. - - ``bouguer_anomaly_complete`` : the Bouguer anomaly including terrain corrections. + - free_air_anomaly : the free-air anomaly in mGal. + - bouguer_anomaly_simple : the Bouguer anomaly without terrain corrections. + - bouguer_anomaly_complete : the Bouguer anomaly including terrain corrections. Will be NaN if no terrain corrections were provided. params : GravityCorrectionParameters A copy of the parameters used to compute corrections and anomalies: - - ``params.ellipsoid`` : the ellipsoid used to compute normal gravity. - - ``params.density_crust`` : the crustal density used in Bouguer corrections. - - ``params.density_water`` : the water density used in Bouguer corrections. - - ``params.spherical_cap_radius`` : the radius of spherical cap used in + - params.ellipsoid : the ellipsoid used to compute normal gravity. + - params.density_crust : the crustal density used in Bouguer corrections. + - params.density_water : the water density used in Bouguer corrections. + - params.spherical_cap_radius : the radius of spherical cap used in computing curvature-corrected form of the Bouguer correction. - - ``params.use_curvature_corrected`` : The type of Bouguer correction used. + - params.use_curvature_corrected : The type of Bouguer correction used. If True, the Bouger correction was the curvature-corrected form, otherwise the infinite planar slab form was used. - - ``params.use_atmospheric_correction`` : If True, atmospheric corrections + - params.use_atmospheric_correction : If True, atmospheric corrections were included in anomaly calculations. tcorr_params : dict[str, TerrainCorrectionParameters] @@ -372,7 +382,7 @@ def __init__( terrain_corrections: TerrainCorrectionData | None = None, ) -> None: self.params: GravityCorrectionParameters - self.tcorr_params: dict[str, GravityCorrectionParameters] = {} + self.tcorr_params: dict[str, TerrainCorrectionParameters] = {} self.data: _pd.DataFrame precomputed_corrections: GravityCorrections | None = None @@ -477,14 +487,10 @@ def __init__( self._compute_bouguer_anomaly() def _compute_free_air_anomaly(self) -> None: - """Compute free-air anomaly and update ``self.data`` + """Compute free-air anomaly and update the instance's ``data`` attribute. This method is called internally during initialization and is not intended to be used directly. - - It could be useful called by a user to recalculate anomalies afer modifying - self.data and/or self.params. - """ cols = ["normal_gravity_at_ellipsoid", "free_air_correction"] self.set_column( @@ -494,14 +500,10 @@ def _compute_free_air_anomaly(self) -> None: ) def _compute_bouguer_anomaly(self) -> None: - """Compute Bouguer anomalies and update ``self.data`` + """Compute Bouguer anomalies and update the instance's ``data`` attribute. This method is called internally during initialization and is not intended to be used directly. - - It could be useful called by a user to recalculate anomalies afer modifying - self.data and/or self.params. - """ cols = self.params.bouguer_correction_fields() tcorr_total_col = "tcorr:total" diff --git a/src/gsolve/reductions/corrections.py b/src/gsolve/reductions/corrections.py index bd10a7c..d318cd8 100644 --- a/src/gsolve/reductions/corrections.py +++ b/src/gsolve/reductions/corrections.py @@ -119,8 +119,7 @@ def normal_gravity_at_ellipsoid( si_units: bool = False, ) -> float | np.ndarray: """ - Calculate normal gravity at the ellipsoid surface using the full Somigliana - formula. + Calculate normal gravity at the ellipsoid surface using the full Somigliana formula. Parameters ---------- @@ -233,8 +232,7 @@ def atmospheric_correction( height_ellipsoidal: ArrayLike, ) -> float | np.ndarray: r""" - Calculate the gravitational effect of the atmospheric mass as a function of - station elevation. + Calculate the gravitational effect of atmospheric mass as a function of station elevation. The atmospheric correction (eqn 3 in Hinze et al.), is given by : @@ -347,8 +345,7 @@ def spherical_bouguer_cap_correction( height_ellipsoidal: ArrayLike, ) -> float | np.ndarray: r""" - Calculate the adjustments to the Bouguer slab correction to account for curvature - of the Earth. + Calculate adjustments to Bouguer slab correction due for curvature of the Earth. The spherical cap correction is given by Hinze (2013): @@ -370,16 +367,16 @@ def spherical_bouguer_cap_correction( spherical_cap_correction : ndarray or float The gravitational effect of the spherical cap in mGal. - Notes - ----- - This function assumes a fixed spherical cap extent of 166735.0 meters, equivalent - to 1.5 degrees of arc on a spherical earth. - See Also -------- bouguer_slab_curvature_corrected : Calculate the full Bouguer correction, including spherical cap correction with customizable cap radius. + Notes + ----- + This function assumes a fixed spherical cap extent of 166735.0 meters, equivalent + to 1.5 degrees of arc on a spherical earth. + References ---------- Hinze WJ, von Frese RRB, Saad AH, (2013). Gravity and Magnetic Exploration: @@ -506,22 +503,23 @@ class GravityCorrectionParameters(GSolveParameters): Parameters ---------- - ellipsoid : "WGS84" or "GRS80" (default) - The reference ellipsoid used in normal gravity - density_crust: float, default = 2670.0 - Density of crust in Mg.m**-3 - density_water: float, default = 1030.0 - Density of water in Mg.m**-3 - spherical_cap_radius: float, defult = 166735.0 - The radius in km of the circular cap correction. The default - 166735.0 km is equivalent to 1.5 degrees of arc for a spherical earth. - use_curvature_corrected: bool, default True - Specify the type of Bouguer correction to compute and use in subsequent - anomaly calculations. If True, bouguer corrections are curvature corrected. - If False, bouguer corrections are for an infinite horizotal slab. - use_atmospheric_correction: bool, default True - Whether to include atmospheric correction in gravity corrections and - subsequent anomaly calculations. + ellipsoid : {"GRS80", "WGS84"}, default "GRS80" + The reference ellipsoid for normal gravity calculations. + density_crust : float, default = 2670.0 + Density of crust in Mg/m-3 + density_water : float, default = 1030.0 + Density of water in Mg/m-3 + spherical_cap_radius : float, default = 166735.0 + The radius in km of the circular cap correction used in spherical + Bouguer corrections. The default 166735.0 km is equivalent to 1.5 degrees + of arc for a spherical earth. + use_curvature_corrected : bool, default True + Specify the type of Bouguer correction to compute. If True, bouguer + corrections are computed for a curved slab of diameter ``spherical_cap_radius``. + If False, bouguer corrections are computed for an infinite horizontal slab. + use_atmospheric_correction : bool, default True + Whether to apply an atmospheric correction when computing gravity corrections and + subsequent anomalies. free_air_gradient : float, default 0.3087691 The free air gradient in mGal/m. """ @@ -535,8 +533,15 @@ class GravityCorrectionParameters(GSolveParameters): free_air_gradient: float = 0.3087691 def bouguer_correction_fields(self) -> list[str]: - """Return tuple of names of correction methods required for - specified Bouguer method.""" + """Corrections required to compute Bouguer correction using specified method. + + Returns + ------- + method_names : list of str + Correction methods mapping to correction functions defined in + ``gsolve.reductions.corrections``. + + """ bc_fields: list[str] = ["normal_gravity_at_ellipsoid", "free_air_correction"] if self.use_atmospheric_correction: bc_fields.append("atmospheric_correction") @@ -545,7 +550,13 @@ def bouguer_correction_fields(self) -> list[str]: return bc_fields def bouguer_correction_type(self) -> str: - """Return the name specified of Bouguer correction method.""" + """Return specified Bouguer correction method. + + Returns + ------- + str + "bouguer_slab_curvature_corrected" or "bouguer_slab_correction" + """ if self.use_curvature_corrected: return "bouguer_slab_curvature_corrected" else: @@ -679,7 +690,13 @@ def __repr__(self) -> str: @classmethod def available_corrections(cls) -> tuple[str, ...]: - """Return tuple of available gravity correction methods""" + """Return tuple of available gravity correction methods. + + Returns + ------- + tuple of str + Names of available gravity correction methods. + """ return ( "normal_gravity_at_stn_elevation", "normal_gravity_at_ellipsoid", @@ -702,9 +719,9 @@ def compute( ---------- sites : GravitySites | DataFrame An object providing site longitude, latitude and ellipsoidal height, and indexed - by ``'site_id'``. If `sites` is a DataFrame, it is expected to have columns named + by 'site_id'. If ``sites`` is a DataFrame, it is expected to have columns named ``'longitude'``, ``'latitude'`` and ``'height_ellipsoidal'``, unless alternative columns are - specified using the `column_names` argument. + specified using the ``column_names`` argument. corrections : str | Sequence[str], optional An array or string of corrections to compute. By default compute all corrections required for generating a Bouguer anomaly as specified in @@ -823,7 +840,7 @@ def compute( return GravityCorrections(params=self.params, site_id=idx, **_df_dict) def _configured_bouguer_corrections(self) -> Sequence[str]: - """Return bouguer corrections based on the current parameters.""" + """Return bouguer correction method names required for the current parameters.""" # noqa: DOC201 corrections = ["normal_gravity_at_ellipsoid", "free_air_correction"] if self.params.use_atmospheric_correction: corrections.append("atmospheric_correction") @@ -836,8 +853,7 @@ def _configured_bouguer_corrections(self) -> Sequence[str]: def bouguer_corrections( self, sites: pd.DataFrame | GravitySites ) -> GravityCorrections: - """Calculate corrections required for computing a Bouguer anomaly as defined in - ``self.params``. + """Calculate corrections required for computing a Bouguer anomaly as defined in ``self.params``. Parameters ---------- @@ -849,7 +865,6 @@ def bouguer_corrections( ------- GravityCorrections Object containing Bouguer corrections and the correction parameters. - """ return self.compute( sites=sites, corrections=self.params.bouguer_correction_fields() diff --git a/src/gsolve/reductions/terrain_corrections.py b/src/gsolve/reductions/terrain_corrections.py index 72f2ef3..c163c51 100644 --- a/src/gsolve/reductions/terrain_corrections.py +++ b/src/gsolve/reductions/terrain_corrections.py @@ -68,7 +68,6 @@ def calculate_terrain_correction( show_progress: bool = True, compute_topography: bool = True, compute_bathymetry: bool = True, - **kwargs, ) -> tuple[np.ndarray, np.ndarray]: """ Calculate terrain corrections for a set of points based on a digital elevation model (DEM). @@ -79,11 +78,11 @@ def calculate_terrain_correction( An list-like object containing the x, y, and z coordinates of the points. dem : xarray.DataArray The digital elevation model (DEM) in meters. Should be in the same coordinate system - and vertical datum as the `points`. + and vertical datum as the ``points``. min_dist : float The minimum distance for the terrain correction mask. Must be >= 0.0. max_dist : float - The maximum distance for the terrain correction mask. Must be > `min_dist`. + The maximum distance for the terrain correction mask. Must be > ``min_dist``. density_dataset : xarray.DataArray, optional An array containing density values in kg/m^3. If not specified, a density model will be generated from the ``dem`` using ``terrain_density``, ``water_density`` @@ -94,7 +93,7 @@ def calculate_terrain_correction( The density of water in kg/m^3, by default 1030.0. sea_level_elevation : float, optional The elevation of sea level in m, by default 0.0. Should be in the same vertical - datum as `dem` and `points`. Used to separate land/topographic and sea/bathymetric + datum as ``dem`` and ``points``. Used to separate land/topographic and sea/bathymetric portions of the DEM. distance_mask_type : {"radial", "rectangular"}, default="radial" The type of distance mask to use. @@ -300,8 +299,8 @@ def tcorr_harmonica_topography( The topographic surface elevations in meters. The DataArray should have no values below 'sea level' topography_density : xarray.DataArray - The density of the topographic surface in kg/m^3, with the same coordinates as `topography`. Must be compatible with - `topography`. + The density of the topographic surface in kg/m^3, with the same coordinates as ``topography``. Must be compatible with + ``topography``. parallel : bool, optional Whether to use parallel processing, by default False. disable_checks : bool, optional @@ -317,7 +316,6 @@ def tcorr_harmonica_topography( harmonica.prism_layer.gravity : the underkying harmonica method used to compute the terrain correction. """ - site_z = point[2] # flip density for cells where elevation above station @@ -366,18 +364,19 @@ def tcorr_harmonica_bathymetry( no values above 'sea_level_elevation'. bathymetry_density : xarray.DataArray An array of 'densities' the material 'filling' bathymetry in kg/m^3, with the - same coordinates as `bathymetry`. For the typical case of oceans, where sea water + same coordinates as ``bathymetry``. For the typical case of oceans, where sea water (1030.0 kg/m^3) is replacing crustal rocks (2670.0 kg/m^3), - ``bathymetry density`` will be the density contrast 1640.0 kg/m^3. Cells where + ``bathymetry_density`` will be the density contrast 1640.0 kg/m^3. Cells where ``bathymetry`` is above ``sea_level_elevation`` must have density set to 0.0, sea_level_elevation : float, optional The elevation of sea level in m, by default 0.0. Should be in the same vertical - datum as `dem` and `points`. Used to separate land/topographic and sea/bathymetric + datum as ``dem`` and ``points``. Used to separate land/topographic and sea/bathymetric portions of the DEM. parallel : bool, optional Whether to use parallel processing, by default False. disable_checks : bool, optional Whether to disable input checks, by default False + Returns ------- float @@ -441,13 +440,13 @@ class TerrainCorrectionParameters(GSolveParameters): dem_source : str, PathLike, default="" Path to a terrain dataset file. This will be loaded during terrain correction computation. If an empty string, then DEM data must be supplied directly to a - ``TerrainCorrector`` instance. Note that `dem_source` input are converted to and + ``TerrainCorrector`` instance. Note that ``dem_source`` inputs are converted to and stored as a string. density_dataset_source : str, PathLike, default="" Path to a density model file, which will be loaded during terrain correction computation. If an empty string, then a simple density model will be generated from the DEMusing ``terrain_density``, ``water_density`` and - ``sea_level_elevation``. Note that `density_dataset_source` inputs are converted + ``sea_level_elevation``. Note that ``density_dataset_source`` inputs are converted to and stored as a string. compute_topography : bool, default is True Compute gravity corrections due to topographic masses. @@ -511,8 +510,9 @@ def to_series( index_name: str | None = "parameter", index_prefix: str | Sequence[str] | None = None, ) -> pd.Series: - """Convert the parameters object to a pandas Series, where index is the parameter - name and values are the parameter values. + """Return parameters as a pandas Series. + + Series values will be indexed by parameter_name. Parameters ---------- @@ -521,10 +521,10 @@ def to_series( index_name : str | None, optional Name for the Series index. If None, the index will be unnamed. index_prefix : str | None, optional - If specified, the returned series will have a MultiIndex where the - first level is `index_prefix`. E.g. if `index_prefix="zone1"`, then + If specified, the returned Series will have a MultiIndex where the + first level is ``index_prefix``. E.g. if ``index_prefix="zone1"``, then the Series index will be of the form: ("zone1", parameter_name,...). - This is useful when combining multiple parameter Series. + This is useful to avoid index collisions when combining parameter Series. Returns ------- @@ -555,13 +555,14 @@ def to_series( return ds def to_dict(self, path2str: bool = False) -> dict[str, Any]: - """Convert the parameters to a dictionary of the form - ``{parameter_name: parameter_value, ...}``. + """Return parameters as a dictionary. + + The dictionary will be of the form ``{parameter_name: parameter_value, ...}``. Parameters ---------- - path2str : bool, optional - If True, convert any Path objects to their string representations. + path2str : bool, default False + If True, convert any Path-like objects to their string representations. Returns ------- @@ -578,7 +579,7 @@ def to_dict(self, path2str: bool = False) -> dict[str, Any]: @classmethod def from_dataframe(cls, df: pd.DataFrame) -> dict[str, Self]: - """Create one or more TerrainCorrectionParameters objects from a DataFrame. + """Generate a dict of TerrainCorrectionParameters objects from a DataFrame. Parameters ---------- @@ -624,23 +625,14 @@ def from_dataframe(cls, df: pd.DataFrame) -> dict[str, Self]: class TerrainCorrector: """ - A class for computing terrain corrections for gravity measurements using digital - elevation models (DEMs). + A class for computing gravity terrain corrections from elevation models (DEMs). It supports multiple calculation "zones", each with its own parameters and data sources. - A typical workflow using this class would be: - - - Define one or more TerrainCorrectionParameters objects for the desired zones. - - Instantiate a TerrainCorrector with these parameters and optional DEM/density models. - - Add additional zones as needed using ``add_calculation_zone``. - - Call ``compute()`` on a set of points. - - Attributes ---------- params : dict - A dictionary of TerrainCorrectionParmeter objects defining the "zones" to be computed. + A dictionary of TerrainCorrectionParameters objects defining the "zones" to be computed. dems : dict[str, DataArray | None] A dictionary storing user supplied DEMs by zone name. Values will be None if no DEM was provided for that zone, in which case the ``dem_source`` attribute of the @@ -654,21 +646,38 @@ class TerrainCorrector: Parameters ---------- - params: TerrainCorrectionParameters | list-like of TerrainCorrectionParameters + params: TerrainCorrectionParameters or list-like of TerrainCorrectionParameters The TerrainCorrectionParameters object(s) defining the "zones" to be computed. - dem : xarray.DataArray | list-like | None, default is None - User supplided DEM(s) corresponding to each zone defined in ``params``. + dem : xarray.DataArray, list-like or None, default is None + User supplied DEM(s) corresponding to each zone defined in ``params``. For example if ``params=[p1, p2, p3]``, and you wish to specify a DEM for p2 only, then the argument must be ``dem=[None, dem_for_p2, None]``. If None, the dem will be loaded from the ``dem_source`` attribute of the corresponding TerrainCorrectionParameters object. If a dem is specified here, then the ``dem_source`` attribute is ignored. - density_model : xarray.DataArray | list-like | None, default is None + density_model : xarray.DataArray, list-like or None, default is None User supplied density model(s) corresponding to each zone defined in ``params``. - For example if ``params=[p1, p2, p3]``, and you wish to specify a density model for - p2 only, then the argument must be ``density_model=[None, model_for_p2, None]``. If None, - the density model will be generated internally based. If a density model is - specified here, then the ``density_dataset_source`` attribute is ignored. + If `density_model` is specified then it must include a density model or None for + each zone in ``params``. For example if ``params=[p1, p2, p3]``, and you wish + to specify a density model for p2 only, then the argument must be + ``density_model=[None, model_for_p2, None]``. + + If ``density_model`` is None, then: + - if ``density_dataset_source`` attribute of the associated + TerrainCorrectionParameters object is not None, the density model will be + loaded from the specified source. + - otherwise generate a default density model from the loaded DEM, using the + ``terrain_density``, ``water_density``, and ``sea_level_elevation`` + attributes of the associated TerrainCorrectionParameters object. + + Notes + ----- + A typical workflow using this class would be: + + - Define one or more TerrainCorrectionParameters objects for the desired zones. + - Instantiate a TerrainCorrector with these parameters and optional DEM/density models. + - Add additional zones as needed using ``add_calculation_zone``. + - Call ``compute()`` on a set of points. """ @@ -745,8 +754,7 @@ def add_zone( dem: xr.DataArray | None = None, density_model: xr.DataArray | None = None, ) -> None: - """Add a terrain correction calculation zone and (optionally) - an associated dem and/or density model. + """Add a terrain correction calculation zone and associated dem and/or density model. Parameters ---------- @@ -801,7 +809,7 @@ def compute( points : GravitySites or sequence of array_likes (x, y, z) The observation points where terrain corrections are to be computed. Must be in the same coordinate reference system as the dem. - If `points` is a `GravitySites` object, then data colums corresponding to + If ``points`` is a ``GravitySites`` object, then data colums corresponding to ``site_xy_fields`` (default: ("easting", "northing")) and ``site_height_field`` (default: "height_ellipsoidal") must have been set. If points is a sequence of array_likes, then it must be of the form @@ -815,9 +823,9 @@ def compute( The terrain correction calculation method to use. Currently only "harmonica" is supported. site_height_field : str, default is "height_ellipsoidal" - When `points` is a `GravitySites` object, get site elevation from this field. + When ``points`` is a ``GravitySites`` object, get site elevation from this field. site_xy_fields : tuple of str, default is ("easting", "northing") - When `points` is a `GravitySites` object, get site x and y coordinates from + When ``points`` is a ``GravitySites`` object, get site x and y coordinates from these fields. Returns @@ -909,7 +917,7 @@ def compute( water_density=pars.water_density, distance_mask_type=pars.distance_mask_type, show_progress=show_progress, - method=method, + # method=method, compute_topography=pars.compute_topography, compute_bathymetry=pars.compute_bathymetry, ) @@ -945,7 +953,7 @@ class TerrainCorrectionData(GSolveTable): """Class to store terrain correction outputs and parameters. In general, a user should not need to instantiate a TerrainCorrectionData object - directly. Instances will be generated from a `TerrainCorrector` object + directly. Instances will be generated from a ``TerrainCorrector`` object via the ``compute()`` method. A TerrainCorrectionData object can be written to a file and then reloaded and re-instantiated, supporting a workflow where terrain corrections need only be computed once. @@ -953,7 +961,7 @@ class TerrainCorrectionData(GSolveTable): Attributes ---------- params : dict - Dictionary of containing copies of the `TerrainCorrectionParameters` objects + Dictionary of containing copies of the ``TerrainCorrectionParameters`` objects used in computing terrain corrections for each zone. For each parameter object, the dictionary key will be ``'tcorr:{obj.name}'``. This naming pattern is also used to label the corresponding terrain correction columns in the @@ -964,9 +972,9 @@ class TerrainCorrectionData(GSolveTable): for each zone, and the total terrain correction. For a given zone defined by TerrainCorrectionParameters object = ``obj``, the output columns will be: - - ``'tcorr:{obj.name}:topo'`` : the topography only component of the terrain + - 'tcorr:{obj.name}:topo' : the topography only component of the terrain correction. Ommited if ``compute_topography`` is False. - - ``'tcorr:{obj.name}:bath'`` : the bathymetry only component of the terrain + - 'tcorr:{obj.name}:bath' : the bathymetry only component of the terrain correction. Ommited if ``compute_bathymetry`` is False. The total terrain correction column will be labeled ``'tcorr:total'``. This is @@ -1106,27 +1114,24 @@ def __init__( def create_empty( cls, site_id: npt.ArrayLike, - **kwargs, ) -> Self: - """Create an empty TerrainCorrectionData object with only site_id and any additional - columns specified in kwargs. + """Create an empty TerrainCorrectionData object. + + The objects ``data`` attribute will be initialized as an empty DataFrame with + "site_id" as index. The object can then be "loaded" with terrain correction data + using the ``set_corrections()`` method. Parameters ---------- site_id : array_like of str - The unique site identifiers as a sequence. All elements are converted to str. Will be - used to index the ``obj.data`` DataFrame. - **kwargs : dict - Additional columns to be included in ``obj.data`` DataFrame. This could include - site location information such as easting, northing, latitude, longitude etc. + The unique site identifiers. All elements are converted to str. Returns ------- TerrainCorrectionData - An empty TerrainCorrectionData object with only site_id and any additional - columns specified in kwargs. + An empty TerrainCorrectionData object. """ - return cls(site_id=site_id, params=None, terrain_corrections=None, **kwargs) + return cls(site_id=site_id, params=None, terrain_corrections=None) def set_corrections( self, @@ -1246,7 +1251,6 @@ def from_dataframe( TerrainCorrectionOutput object. """ - # fist get the parameters # this informs whet to expect from the dataframe if isinstance(params, pd.DataFrame): @@ -1320,12 +1324,12 @@ def from_dataframe( return obj def _params_to_dataframe(self, paths_to_str: bool = True) -> pd.DataFrame: - """Convert the parameters to a DataFrame.""" + """Convert the parameters to a DataFrame.""" # noqa: DOC201 params_df = self._params_to_series().to_frame() return params_df.reset_index() def _params_to_series(self) -> pd.Series: - """Convert the parameters to a Series.""" + """Convert the parameters to a Series.""" # noqa: DOC201 pars = [] for k, v in self.params.items(): # TerrainCorrectionParameters.to_series() @@ -1361,12 +1365,8 @@ def to_excel( if_sheet_exists : IfSheetExists, default='error' Action to take if the sheet already exists. Options are: 'error', 'replace', or 'overlay'. - **kwargs : dict - Additional keyword arguments passed to `pandas.DataFrame.to_excel`. - - Returns - ------- - None + kwargs : dict + Additional keyword arguments passed to ``pandas.DataFrame.to_excel``. """ kwargs["header"] = kwargs.get("header", True) @@ -1424,8 +1424,8 @@ def from_excel( params_sheet_name : str, optional The name of the excel worksheet from which to read terrain correction parameters. If not specified then ``'{sheet_name} Params'`` will be used. - **kwargs : dict - Additional keyword arguments passed to `pandas.read_excel`. + kwargs : dict + Additional keyword arguments passed to ``pandas.read_excel``. Returns ------- @@ -1455,8 +1455,8 @@ def to_csv(self, fname: FilePath | None = None, **kwargs) -> str | None: ---------- fname : str or PathLike | None The path to the output CSV file. If None, then the CSV string is returned. - **kwargs : dict - Additional keyword arguments passed to `pandas.DataFrame.to_csv`. + kwargs : dict + Additional keyword arguments passed to ``pandas.DataFrame.to_csv``. Returns ------- @@ -1491,9 +1491,9 @@ def from_csv( Parameters ---------- fname : str or PathLike - The path to the input CSV file. - **kwargs : dict - Additional keyword arguments passed to `pandas.read_csv`. + The input CSV file. + kwargs : + Additional keyword arguments passed to ``pandas.read_csv``. Returns ------- diff --git a/src/gsolve/reports.py b/src/gsolve/reports.py index 26c84c3..4293718 100644 --- a/src/gsolve/reports.py +++ b/src/gsolve/reports.py @@ -16,8 +16,10 @@ # Copyright (c) 2025 Earth Sciences New Zealand. +from copy import deepcopy + from pathlib import Path -from typing import Any +from typing import Any, Self import pandas as _pd @@ -35,38 +37,47 @@ class GSolveReport: - """Class for summarising and reporting results of a simnge GSolve network adjustment. + """Class for summarising and reporting results of a gSolve network adjustment. This class provides a simple interface for collating the various inputs - and outputs from a GSolve run. The report can be - printed to the console or saved to an Excel workbook. + and outputs from a gSolve run. The report can be written to an Excel workbook. Parameters ---------- - observations : GravityObservations - The observations used in the GSolve network adjustment. - survey : GravitySurvey - The survey associated with the observations. + observations : GravityObservations | GravitySurvey + The observations used in the gSolve network adjustment. + sites: GravitySites | GravitySurvey + The sites associated with the observations. results : GSolveResults - The results of a GSolve network adjustment. + The results of a gSolve network adjustment. anomalies : GravityAnomalies, optional - The gravity anomalies calculated from the observations. + The gravity standard corrections for each site and gravity anomalies + calculated from adjusted gravity results. terrain_corrections: TerrainCorrectionData, optional - Terrain corrections for sites. If terrain corrections are included in the - anomalies object, these will be used to populate the terrain correction data + Terrain corrections for sites. Not required if terrain correction data + are already included in the ``anomalies`` object. Attributes ---------- obs_data : Dataframe - Gravity observation inputs and outputs. + Gravity observation input data and network adjusted outputs. site_data : Dataframe - All site related data, including inputs, solutions, normal gravity + All site related data, including input data, site solutions, normal gravity corrections and anomalies. loop_data : Dataframe - Gsolve solutions by loop. + gSolve network adjustment solutions for each loop. + terrain_correction_data : Dataframe + Terrain corrections for each site, if provided. params : dict - The parameters objects for "observations", "sites", "solution", "anomalies" - and terrain corrections. + A dictionary of objects storing the various parameters used in + reducing input data, performing network adjustment and calculating anomalies. + The dictionary provides the following keys: + + - 'observations' : GravityObservationsParameters. + - 'solution' : GSolveSolutionParameters. + - 'anomalies' : GravityCorrectionParameters, if ``anomalies`` argument provided. + - 'terrain_corrections': dict of terrain correction parameters of the form + {"zone_id": TerrainCorrectionParameters}. """ @@ -75,8 +86,8 @@ def __init__( observations: GravityObservations | GravitySurvey, sites: GravitySites | GravitySurvey, results: GSolveResults, - terrain_corrections: TerrainCorrectionData | None = None, anomalies: GravityAnomalies | None = None, + terrain_corrections: TerrainCorrectionData | None = None, ) -> None: self.site_data: _pd.DataFrame self.obs_data: _pd.DataFrame @@ -111,6 +122,14 @@ def __init__( self._set_loop_data(results=results, obs_input=observations) self._set_terrain_correction_data(terrain_corrections=terrain_corrections) + def copy(self) -> Self: + """Return a deep copy.""" # noqa: DOC201 + return self.__copy__() + + def __copy__(self) -> Self: + """Return a deep copy.""" # noqa: DOC201 + return deepcopy(self) + def _set_params( self, observations: GravityObservations, @@ -118,6 +137,7 @@ def _set_params( results: GSolveResults, anomalies: GravityAnomalies | None = None, ) -> None: + self.params["observations"] = observations.params() if hasattr(sites, "params"): self.params["sites"] = sites.params.copy() # ty:ignore[unresolved-attribute] @@ -136,8 +156,19 @@ def _set_site_data( results: GSolveResults, anomalies: GravityAnomalies | None, ) -> None: - """Set self.site_data DataFrame by merging GravitySites, GSolveResults and - GravityAnomalies objects. + """ + Set site_data attribute from site, observation, network adjustment data. + + Parameters + ---------- + site_input : GravitySites + Gravity site data. + obs_input : GravityObservations + Gravity observation data. + results : GSolveResults + Results of gSolve network adjustment. + anomalies : GravityAnomalies, optional + Standard gravity anomalies and corrections for each site. """ merge_kwargs = dict( left_index=True, @@ -277,36 +308,32 @@ def to_excel( filename: FilePath, if_workbook_exists: IfWorkbookExists = "error", if_sheet_exists: IfSheetExists = "error", + **kwargs: Any, # noqa: ANN401 ) -> None: """ - Save 'report' to an Excel file. + Save the report data to an Excel file. Parameters ---------- filename : str or PathLike - Write the Excel workbook to `filename`. + Write the report to ``filename``. if_workbook_exists : {'error', 'append', 'replace'}, default 'error' - Behaviour if `filename` already exists: + Behaviour if ``filename`` already exists: - - ``'error'`` : raise an error if the workbook already exists. - - ``'append'`` : attempt append worksheets to an existing workbook. - - ``'replace'`` : overwrite the existing workbook. + - 'error' : raise an error if the workbook already exists. + - 'append' : attempt append worksheets to an existing workbook. + - 'replace' : overwrite the existing workbook. if sheet_exists : {'error', 'replace', 'new'}, default 'error' Behaviour if the worksheet already exists (Only applicable when ``if_workbook_exists='append'``) - - ``'error'``: raise a ValueError - - ``'replace'`` : overwrite workseet. - - ``'new'`` : create a new worksheet with a different name. + - 'error': raise a ValueError + - 'replace' : overwrite workseet. + - 'new' : create a new worksheet with a different name. + **kwargs : + Arguments to be passed to ``DataFrame.to_excel()`` method. """ - - # used after writing initial worksheet, because then workbook exists - write_excel_kwargs: dict = { - "if_workbook_exists": "append", - "if_sheet_exists": if_sheet_exists, - } - sheets_to_write = ["observations", "sites", "loops", "metadata"] filename = Path(filename) @@ -329,52 +356,59 @@ def to_excel( # observations write_excel_worksheet( - prepare_writable_df( - self.obs_data, + df=prepare_writable_df( + df=self.obs_data, expand_datetime="datetime", drop_datetime=False, bool_to_int=True, ), - filename, + filename=filename, sheet_name="observations", if_workbook_exists=if_workbook_exists, if_sheet_exists=if_sheet_exists, + **kwargs, ) + # if_workbook_exists, set to "append" for subsequent sheets, as workbook will now exist + # sites write_excel_worksheet( - prepare_writable_df( - self.site_data, + df=prepare_writable_df( + df=self.site_data, expand_datetime=None, bool_to_int=True, ), - filename, + filename=filename, sheet_name="sites", - **write_excel_kwargs, + if_workbook_exists="append", + if_sheet_exists=if_sheet_exists, + **kwargs, ) # loop write_excel_worksheet( - prepare_writable_df(self.loop_data), - filename, + df=prepare_writable_df(self.loop_data), + filename=filename, sheet_name="loops", - **write_excel_kwargs, + if_workbook_exists="append", + if_sheet_exists=if_sheet_exists, ) if self.terrain_correction_data is not None: write_excel_worksheet( - prepare_writable_df(self.terrain_correction_data), - filename, + df=prepare_writable_df(self.terrain_correction_data), + filename=filename, sheet_name="terrain_corrections", - **write_excel_kwargs, + if_workbook_exists="append", + if_sheet_exists=if_sheet_exists, + **kwargs, ) - # parameters - write_metadata_kwargs = write_excel_kwargs.copy() - write_metadata_kwargs.update(index=False) - + # Write all parameters to a single spreadsheet all_params = [] + # This is a kludge - should create method on parameter objects to + # to normalise parameter outputs for writing to excel. def _format_value(x: Any) -> str | float | int | bool: # noqa: ANN401 if isinstance(x, _pd.Timedelta): return x.total_seconds() @@ -382,19 +416,25 @@ def _format_value(x: Any) -> str | float | int | bool: # noqa: ANN401 return str(x) return x - for section, param in self.params.items(): + for section, param_obj in self.params.items(): df: _pd.DataFrame = ( - param.to_series(series_name="value", index_name="parameter") + param_obj.to_series(series_name="value", index_name="parameter") .to_frame() .reset_index() - ) + ).copy() df.iloc[:, 1] = df.iloc[:, 1].map(_format_value) df.insert(0, "section", section) all_params.append(df) + if "index" in kwargs: + _ = kwargs.pop("index") + write_excel_worksheet( - _pd.concat(all_params), - filename, + df=_pd.concat(all_params), + filename=filename, sheet_name="metadata", - **write_metadata_kwargs, + if_workbook_exists="append", + if_sheet_exists=if_sheet_exists, + index=False, + **kwargs, ) diff --git a/src/gsolve/scintrex.py b/src/gsolve/scintrex.py index 61d4ae9..5294708 100644 --- a/src/gsolve/scintrex.py +++ b/src/gsolve/scintrex.py @@ -50,7 +50,7 @@ class ScintrexData(abc.ABC): - """Base class for Scintrex data files.""" + """Base class for classes that read and manipulate Scintrex data.""" def __init__( self, @@ -59,9 +59,6 @@ def __init__( metadata_units: dict[str, str] | None = None, on_error: Literal["raise", "warn", "ignore"] = "raise", ) -> None: - """ - Base class for reading and prcoessing Scintrex data files. - """ self.data: pd.DataFrame self.metadata: dict[str, _ScintrexMetadataDataTypes] self.metadata_units: dict[str, str] @@ -70,11 +67,11 @@ def __init__( self._set_data(data, on_error) @abc.abstractmethod - def to_gsolve_observations(self) -> GravityObservations: + def to_gsolve_observations(self) -> GravityObservations: # noqa: D102 pass @abc.abstractmethod - def set_loop(self) -> None: + def set_loop(self) -> None: # noqa: D102 pass @abc.abstractmethod @@ -120,6 +117,7 @@ def __copy__(self) -> Self: return deepcopy(self) def copy(self) -> Self: + """Return a deep copy of the object.""" # noqa: DOC201 return self.__copy__() @@ -157,7 +155,7 @@ class CG6Data(ScintrexData): metadata : dict Metadata from file headers converted to approiate dtypes, with field names normalized to lowercase. Measurement units stored as a suffix to the field - name (e.g. "fieldname [unit]") are removed and stored in the `'metadata_units'` + name (e.g. "fieldname [unit]") are removed and stored in the ``metadata_units`` attribute. metadata_units : dict The measurement units for metadata fields. @@ -248,7 +246,7 @@ def _set_metadata( metadata: dict[str, _ScintrexMetadataDataTypes], metadata_units: dict[str, str] | None = None, ) -> None: - """Set metadata and metadata_units attributes""" + """Set metadata and metadata_units attributes.""" self.metadata: dict[str, _ScintrexMetadataDataTypes] = {} self.metadata_units: dict[str, str] = {} @@ -326,7 +324,7 @@ def _set_data(self, data: pd.DataFrame, on_error: str = "raise") -> None: self.data = df def _strip_corrections(self) -> pd.Series: - """Return corrgrav values with all corrections removed.""" + """Return corrgrav values with all corrections removed.""" # noqa: DOC201 return ( self.data["corrgrav"] - (self.data["driftcorr"] * self.data["correction_drift"]) @@ -448,7 +446,7 @@ def set_loop( Increment loop identifier by ``loop_step``. loop_format : str, default '{LOOP}' Format string for loop identifiers. Use 'LOOP' as a placeholder - for the loop number. The default `"{LOOP}"` is effectively no + for the loop number. The default ``"{LOOP}"`` is effectively no formatting. Using, for example, ``loop_format="x_{'LOOP':02d}_y"`` would produce loop id's ``'x_01_y', 'x_02_y', ...``. output_column : str, default 'loop' @@ -487,7 +485,7 @@ def set_loop( if datetimes is not None: if isinstance(datetimes, pd.Series): dates = to_naive_utc_datetime(datetimes.index) - loop_ids = datetimes.astype(str).values + loop_ids = datetimes.astype(str).to_list() elif isinstance(datetimes, Mapping): dates = to_naive_utc_datetime(list(datetimes.keys())) loop_ids = [str(l) for l in datetimes.values()] @@ -641,11 +639,11 @@ def to_gsolve_sites( coords_source : {'user', 'gps'}, default 'user' Specify the source of lat, lon and elev data: - - ``'gps'`` : the mean of 'latgps', 'longps' and 'elevgps' + - 'gps' : the mean of 'latgps', 'longps' and 'elevgps' for each site. These positions are derived from the internal GPS reciever and are of low accuracy, but are almost certainly correct to within a few 10's of metres. - - ``'user'`` : take values from 'latuser', 'lonuser' and 'elevuser' + - 'user' : take values from 'latuser', 'lonuser' and 'elevuser' for each site. The 'user' coords are sourced from the instrument file ``stations.dat``. This file can be pre-populated with accurate station coordinates prior to field data collection, however there is no @@ -738,7 +736,7 @@ def set_drift_correction( def _slurp_scintrex_text_file(filepath: FilePath) -> list[str]: - """Read a Scintrex text file, fix encoding and return lines as a list.""" + """Read a Scintrex text file, fix encoding and return lines as a list.""" # noqa: DOC201 with open(filepath, "r", encoding="utf-8-sig") as fh: return [l.strip() for l in fh.readlines()] @@ -748,7 +746,7 @@ def _split_header_key_val_unit( normalize_key: bool = True, extract_units: bool = True, ) -> tuple[str, str, str]: - """Split headers into key, value and units.""" + """Split headers into key, value and units.""" # noqa: DOC201 header = header.strip("/ ") if not header: return ("", "", "") @@ -799,7 +797,7 @@ def _scintrex_header_type_conversion( def _extract_unit_from_keyword(header: str) -> tuple[str, str]: - """Get header and unit form a header string.""" + """Get header and unit form a header string.""" # noqa: DOC201 if header.endswith(")"): sep = "(" elif header.endswith("]"): diff --git a/src/gsolve/sites.py b/src/gsolve/sites.py index fc7f35d..78330f7 100644 --- a/src/gsolve/sites.py +++ b/src/gsolve/sites.py @@ -57,41 +57,54 @@ class GravitySites(GSolveTable): """Class to store gravity site/station data and metadata. - Attributes - ---------- - data : DataFrame - Gravity site/station information stored as a pandas DataFrame. - The data fields (i.e. columns) required by GSolve are have explicitly - defined names, dtypes and default values. The preferred method for setting - fields is to use the :meth:`gsolve.core.data.GSolveTable.set_columns` method. - Other fields may be added to ``obj.data`` as required, but will - be ignored by gsolve. - - The defined fields are: - - * Fields that must be defined at object creation: - - - `site_id`, index, *str*: unique station identifier. - - `latitude`, *float*: station latitude in decimal degrees. - - `longitude`, *float*: station longitude in decimal degrees. - - `height_ellipsoidal`, *float*: elevation relative to the ellipsoid. - - * Fields that will be created with default values if not specified at object creation - - - `reference_gravity`, *float*: reference gravity value at that site, NaN - if unknown. Typically absolute gravity, but could be set to some arbitrary - value if no reference gravity data are available. At least one `reference_gravity` - value must be set for solve for drift. - - `gsolve_tie`, *bool*: indicates whether this site is to be used as - "tie" when solving for drift. At least one site with a `reference_gravity` - value must be set as a `gsolve_tie`. - - * Cartesian coordinates. These are not required for gsolve, - but are required for calculating terrain corrections. - - - easting, *float*: site locations in some cartesian coordinate system - - northing, *float*: site locations in some cartesian coordinate system - - height_orthometric, *float*: height of site above some datum + Gravity site/station information stored as a pandas DataFrame. + The data fields (i.e. columns) required by GSolve have explicitly + defined names, dtypes and default values. The preferred method for setting + fields is to use the :meth:`obj.set_column` method. + Other fields may be added to ``obj.data`` as required, but will + be ignored by gsolve. + + The defined fields are: + + **Mandatory Fields** - Must be defined at object creation. + + ==================== ======= ======================================================= + Name Type Description + ==================== ======= ======================================================= + site_id *str* unique site identifier, set as obj.data.index + latitude *float* site latitude in decimal degrees. + longitude *float* site longitude in decimal degrees. + height_ellipsoidal *float* site elevation relative to the ellipsoid in meters. + ==================== ======= ======================================================= + + **Required Fields** - Will be created with default values if not specified. Must be + at least partially set for computing drift. + + ======================= ======= ========= =============================================== + Name Type Default Description + ======================= ======= ========= =============================================== + reference_gravity *float* ``NaN`` Reference gravity value at that site. Typically + absolute gravity, but could be set to some + arbitrary value if no reference gravity data + are available. At least one reference_gravity + value must be set for solve for drift. + gsolve_tie *bool* ``False`` Indicates whether a site is to be used as "tie" + when solving for drift. At least one site with + a reference_gravity value must be set as a + gsolve_tie. + ======================= ======= ========= =============================================== + + + **Cartesian coordinates** - Not required for network adjustment, but are required + for calculating terrain corrections. + + ==================== ======= ======================================================= + Name Type Description + ==================== ======= ======================================================= + easting *float* site locations in some cartesian coordinate system + northing *float* site locations in some cartesian coordinate system + height_orthometric *float* height of site above some datum + ==================== ======= ======================================================= Parameters ---------- @@ -181,20 +194,21 @@ def from_excel( excel_file: FilePath, sheet_name: str | int | list[int | str] | None = None, ignore_unknown_fields: bool = True, - parse_split_datetime: bool = True, mapper: Renamer | None = None, **kwargs, - ) -> "GravitySites": - """Read a GravitySites object from an excel workbook. + ) -> Self: + """ + Create a GravitySites object from an excel workbook. Parameters ---------- excel_file : str or PathLike The excel workbook to read from. - sheet_name : str | int, optional - The worksheet name or location within ``excel_file``. If not - specified. attempt to read from the standard sheet name 'sites' - and then from the legacy sheet name 'Locations' + sheet_name : str, int or array-like, optional + The worksheet to read specified as name (str) or sheet index (int,0-based). + If an array-like is provided, attempt to read the first existing worksheet. + If sheet_name is not specified, then attempt to read from the standard + worksheet names ["sites", "Locations"]. ignore_unknown_fields : bool, default True If True, columns that have no defined specification are dropped. Use ``GravitySites.known_fields()`` to return a list of the @@ -203,7 +217,7 @@ def from_excel( Dict-like or function transformations to apply to column names before creating object. See ``DataFrame.rename`` method for full documentation kwargs : - Arguments passed to ``pandas.read_excel`` method. + Keyword arguments are passed to ``pandas.read_excel`` method. Returns ------- @@ -241,7 +255,7 @@ def get_ties( active_only : bool, optional, default is True Only , by default True gravity_only : bool, optional, default is True - If `True` only return reference_gravity values, otherwise + If True only return reference_gravity values, otherwise return all fields. Returns @@ -403,7 +417,23 @@ def _get_writable_df( bool_to_int: bool = True, include_unknown_fields: bool = False, ) -> _pd.DataFrame: - """Return a DataFrame suitable for writing to a file.""" + """ + Return GravitySite data as a DataFrame suitable for writing to an excel or csv file. + + Parameters + ---------- + normalize_column_names : bool, default=True + If True, convert column names to snake case. + bool_to_int : bool, default=True + If True, convert boolean True/False to 1/0. + include_unknown_fields : bool, default=False + If True, include columns that are not defined as known fields. + + Returns + ------- + DataFrame + A DataFrame suitable for writing to a file. + """ cols: list[str] = [ str(c) for c in self.known_fields() if c in self.data.columns ] @@ -418,32 +448,30 @@ def _get_writable_df( def write_to_csv( self, - fname: FilePath, + csv_file: FilePath, normalize_column_names: bool = True, - expand_datetime: str | None = None, - drop_datetime: bool = False, bool_to_int: bool = True, include_unknown_fields: bool = False, **kwargs, ) -> None: - """Write `data` DataFrame to csv file. + """Write data DataFrame to csv file. Parameters ---------- csv_file : str or PathLike - The path to the excel file. + The name/path of the csv file to write to. normalize_column_names : bool, default True Convert columns name to snake case. bool_to_int : bool, default True Convert boolean True/False to 1,0. include_unknown_fields : bool, default False - Include fields not in the known fields. - **kwargs - Additional keyword arguments passed to `pandas.DataFrame.to_csv`. + Include columns that are not defined as known fields. + kwargs + Additional keyword arguments passed to ``pandas.DataFrame.to_csv``. See Also -------- - pandas.DataFrame.to_csv + ``pandas.DataFrame.to_csv`` The underlying function used to write the DataFrame to a csv. """ @@ -454,7 +482,7 @@ def write_to_csv( normalize_column_names=normalize_column_names, bool_to_int=bool_to_int, include_unknown_fields=include_unknown_fields, - ).to_csv(fname, **kwargs) + ).to_csv(csv_file, **kwargs) def to_excel( self, @@ -467,14 +495,15 @@ def to_excel( if_sheet_exists: IfSheetExists = "error", **kwargs, ) -> None: - """Write `data` DataFrame to an excel file. + """Write data DataFrame to an excel file. Parameters ---------- excel_file : str or PathLike The excel workbook to write to. sheet_name : str, default None - The name of the worksheet to write to. + The name of the worksheet to write to. If not specifed then use the + default sheet name "sites". normalize_column_names : bool, default True Convert columns name to snake case. bool_to_int : bool, default True @@ -485,14 +514,14 @@ def to_excel( Behaviour if the excel file already exists. if_sheet_exists : {"error", "replace", "new"}, default "error" Behaviour if the worksheet already exists. - **kwargs - Additional keyword arguments passed to `pandas.DataFrame.to_excel`. + kwargs + Additional keyword arguments passed to ``pandas.DataFrame.to_excel``. See Also -------- gsolve.core.excel_io._core_excel_io.write_excel_worksheet - For complete explanation of parameters `if_workbook_exists` - and `if_sheet_exists`. + For complete explanation of parameters ``if_workbook_exists`` + and ``if_sheet_exists``. pandas.DataFrame.to_excel The underlying function used to write the DataFrame to the excel file. @@ -513,7 +542,7 @@ def to_excel( bool_to_int=bool_to_int, include_unknown_fields=include_unknown_fields, ), - excel_file=excel_file, + filename=excel_file, sheet_name=sheet_name, if_workbook_exists=if_workbook_exists, if_sheet_exists=if_sheet_exists, @@ -533,10 +562,10 @@ def sample_elevation( Parameters ---------- dem : xarray.DataArray, xarray.Dataset, str or PathLike - The array of values to sample. If `dem` is not a + The array of values to sample. If ``dem`` is not a output_col : str or None, default None - If `output_col` is defined, write sampled values to `obj.data[output_col]`. - If `output_col` is None, return a Series of sampled values. + If ``output_col`` is defined, write sampled values to obj.data[output_col]. + If ``output_col`` is None, return a Series of sampled values. xcol : str, optional The column holding x coordinates, by default "easting" ycol : str, optional @@ -548,7 +577,7 @@ def sample_elevation( Returns ------- elevations : Series or None - None if `output_col` is defined, otherwise a Series of sampled elevations. + None if ``output_col`` is defined, otherwise a Series of sampled elevations. """ if isinstance(dem, FilePath): @@ -576,6 +605,22 @@ def get_points( ) -> tuple[ _npt.NDArray[_np.float64], _npt.NDArray[_np.float64], _npt.NDArray[_np.float64] ]: + """Get site point coordinates as numpy arrays. + + Parameters + ---------- + xcol : str + The column name for x coordinates. + ycol : str + The column name for y coordinates. + zcol : str or None, optional + The column name for z coordinates. If None, z coordinates will be set to NaN. + + Returns + ------- + tuple of numpy.ndarray + The x, y, and z coordinates as numpy arrays. + """ x = self.data.loc[:, xcol].to_numpy().copy().astype(_np.float64) y = self.data.loc[:, ycol].to_numpy().copy().astype(_np.float64) if zcol is None: @@ -593,19 +638,6 @@ def _siteid_exists(site_id: str, other: _pd.DataFrame | GSolveTable) -> bool: class ReferenceGravity(GSolveTable): """Class providing a simple mechanism for merging reference gravity data. - Attributes - ---------- - data : DataFrame - The reference gravity data indexed by `site_id`. The defined fields - are: - - - ``'gravity'`` : (float) The reference gravity value for the site. - - ``'active'`` : (bool) Indicates whether the site should be used - as an active "gsolve_tie" when merged into a ``GravitySites``. - - Other fields may be added to ``obj.data`` as required, but will - be ignored by gsolve. - Parameters ---------- site_id : array_like @@ -615,9 +647,21 @@ class ReferenceGravity(GSolveTable): active, array_like or bool, default True An array indicating whether a site should be set as an active "gsolve_tie" when merged into a ``GravitySites``. - **kwargs : dict[str, array_like] + kwargs Additional fields to be added to the site data. + Attributes + ---------- + data : DataFrame + The reference gravity data indexed by ``site_id``. The defined fields + are: + + - 'gravity' : (float) The reference gravity value for the site. + - 'active' : (bool) Indicates whether the site should be used + as an active "gsolve_tie" when merged into a ``GravitySites`` object. + + Other fields may be added to ``obj.data`` as required, but will + be ignored by gsolve. """ _known_fields: dict[str, DataFieldSpecification] = { @@ -633,7 +677,7 @@ def __init__( site_id: _npt.ArrayLike, gravity: _npt.ArrayLike, active: _npt.ArrayLike | bool = True, - **kwargs: dict[str, _npt.ArrayLike], + **kwargs: _npt.ArrayLike, ) -> None: _site_id = to_1d_ndarray(site_id).astype(str) @@ -681,7 +725,23 @@ def _get_writable_df( include_unknown_fields: bool = False, bool_to_int: bool = True, ) -> _pd.DataFrame: - """Return a DataFrame suitable for writing to a file.""" + """ + Return GravitySite data as a DataFrame suitable for writing to an excel or csv file. + + Parameters + ---------- + normalize_column_names : bool, default=True + If True, convert column names to snake case. + bool_to_int : bool, default=True + If True, convert boolean True/False to 1/0. + include_unknown_fields : bool, default=False + If True, include columns that are not defined as known fields. + + Returns + ------- + DataFrame + A DataFrame suitable for writing to a file. + """ cols = [c for c in self.known_fields() if c in self.data.columns] if include_unknown_fields: cols.extend(c for c in self.data.columns if c not in cols) @@ -694,21 +754,39 @@ def _get_writable_df( def write_to_csv( self, - fname: FilePath, + csv_file: FilePath, normalize_column_names: bool = True, - expand_datetime: str | None = None, - drop_datetime: bool = False, - bool_to_int: bool = True, + bool_to_int: bool = True, include_unknown_fields: bool = False, **kwargs, ) -> None: + """Write data to a csv file. + + Parameters + ---------- + csv_file : str or PathLike + The path to the csv file. + normalize_column_names : bool, default True + Convert columns name to snake case. + bool_to_int : bool, default True + Convert boolean True/False to 1,0. + include_unknown_fields : bool, default False + Include fields not in the known fields. + kwargs + Additional keyword arguments passed to ``pandas.DataFrame.to_csv``. + + See Also + -------- + pandas.DataFrame.to_csv + The underlying function used to write the DataFrame to a csv. + """ kwargs["header"] = kwargs.get("header", True) kwargs["index"] = kwargs.get("index", True) self._get_writable_df( normalize_column_names=normalize_column_names, bool_to_int=bool_to_int, include_unknown_fields=include_unknown_fields, - ).to_csv(fname, **kwargs) + ).to_csv(csv_file, **kwargs) def to_excel( self, @@ -725,7 +803,7 @@ def to_excel( Parameters ---------- - fname : str or PathLike + excel_file : str or PathLike The path to the excel file. sheet_name : str, default None The name of the worksheet to write to. @@ -740,13 +818,13 @@ def to_excel( if_sheet_exists : {"error", "replace", "new"}, default "error" Behaviour if the worksheet already exists. **kwargs - Additional keyword arguments passed to `pandas.DataFrame.to_excel`. + Additional keyword arguments passed to ``pandas.DataFrame.to_excel``. See Also -------- gsolve.core.excel_io.write_excel_worksheet - For complete explanation of parameters `if_workbook_exists` - and `if_sheet_exists`. + For complete explanation of parameters ``if_workbook_exists`` + and ``if_sheet_exists``. pandas.DataFrame.to_excel The underlying function used to write the DataFrame to the excel file. @@ -767,7 +845,7 @@ def to_excel( bool_to_int=bool_to_int, include_unknown_fields=include_unknown_fields, ), - excel_file=excel_file, + filename=excel_file, sheet_name=sheet_name, if_workbook_exists=if_workbook_exists, if_sheet_exists=if_sheet_exists, @@ -791,6 +869,8 @@ def from_dict( Reference site data as a dictionary where keys are ``'site_id'`` and values are either the reference gravity (float) or a sequence of (reference gravity, active) where active is a boolean + set_active : bool, default True + If a ``data`` value is a float, then set the "active" field to this value. Returns ------- diff --git a/src/gsolve/tide/earth_tide.py b/src/gsolve/tide/earth_tide.py index ebaa782..d5ae18c 100644 --- a/src/gsolve/tide/earth_tide.py +++ b/src/gsolve/tide/earth_tide.py @@ -23,12 +23,10 @@ import numpy as _np import pandas as _pd import pygtide -from matplotlib.pyplot import axis from numpy import arccos, arcsin, arctan, cos, deg2rad, pi, sin, sqrt from numpy.typing import ArrayLike, NDArray -from pandas.api.interchange import from_dataframe from pandas.api.typing import NaTType -from PIL.Image import new + from gsolve.core._typing import ( DatetimeArray, @@ -59,7 +57,7 @@ class EarthTideCorrectionProvider(Protocol): """Protocol defining interface for classes that provide earth tide corrections.""" - def tidal_correction( + def tidal_correction( # noqa: D102 self, lat: FloatArray, lon: FloatArray, @@ -69,7 +67,7 @@ def tidal_correction( **kwargs, ) -> NDArray[_np.float64]: ... - def identifier(self, **kwargs) -> str: ... + def identifier(self, **kwargs) -> str: ... # noqa: D102 def gravimetric_factor( @@ -88,7 +86,7 @@ def gravimetric_factor( Returns ------- delta2: ndarray or float - The gravimetric factor, float if both `k2` & `h2` are + The gravimetric factor, float if both ``k2`` and ``h2`` are floats. References @@ -183,6 +181,12 @@ def __repr__(self) -> str: return f"{self.__class__.__name__}(amp_factor={self.amp_factor})" def identifier(self, **kwargs) -> str: + """Return an identifier string for this LongmanTidalCorrection instance. + + Returns + ------- + str + """ if not kwargs: return self.__repr__() @@ -210,12 +214,7 @@ def gravity_accelerations( Elevation in meters (datum independent) dt : str, datetime, or array_like of shape(M,) The date-times to be at which to calculate corrections. Can - be any format parsable by Pandas.to_datetime() method. - amp_factor : float, or array_like of shape(M,), optional - Factor to apply to tidal corrections to account for earth's - deformation response to tidal forces. Tyypically in range - 1.14-1.2 for semi-diurnal tides (default 1.2). To calculate this factor - using h2 and k2 parameters the gravimetric_factor function can be used. + be any format parsable by ``pandas.to_datetime()`` method. Returns ------- @@ -412,6 +411,9 @@ def tidal_correction( Elevation in meters date_time : array_like The datetimes at which to calculate corrections. + site_id : array_like, optional + This argument is required to match the signature of the + ``EarthTideCorrectionProvider`` protocol, but is ignored in this implementation. Returns ------- @@ -527,11 +529,13 @@ def _decimal_julian_century( The date-times to be converted. Can be any format parsable by ``pandas.to_datetime`` method. Datetimes without a timezone are assumed to be in UTC. + kwargs : + Additional keyword arguments are passed to ``to_naive_utc_datetime``. Returns ------- julian_century : ndarray or scalar - This is scalar if `dt` is a single value. + This is scalar if ``dt`` is a single value. See Also -------- @@ -579,7 +583,7 @@ def _decimal_hour_of_day( Returns ------- hour : ndarray or scalar - The decimal hour. This is a scalar if `arg` is a single value. + The decimal hour. This is a scalar if ``date_time`` is a single value. """ _dt = to_naive_utc_datetime(date_time, allow_nat=False) @@ -607,21 +611,16 @@ class EternaTidalParameters: define the ampltude factors and phase leads to be applied to the tidal potentials provided by a given tidal catalogue. A single tidal parameter is comprises 4 values - - ``freq_start``: the start frequency of the tidal constituent in cycles per day (cpd). - - ``freq_stop``: the stop frequency of the tidal constituent in cpd. - - ``amplitude_factor``: multiply the amplitude of the tidal constituent. - - ``phase_lead``: the phase lead in degrees + - 'freq_start': the start frequency of the tidal constituent in cycles per day (cpd). + - 'freq_stop': the stop frequency of the tidal constituent in cpd. + - 'amplitude_factor': multiply the amplitude of the tidal constituent. + - 'phase_lead': the phase lead in degrees An example tidal parameter (from ETERNA documentation) covering M2, the semi-diurnal lunar tide constituent is ``[1.914129, 1.950419, 1.18705, 2.0327]``. Multiple tidal parameters can be combined to cover the full spectrum of tidal constituents. - Attributes - ---------- - data : pd.DataFrame - A DataFrame containing the tidal parameters under the columns 'freq_start', - 'freq_stop', 'amplitude', and 'phase_lead'. Other columns such as 'wave_group' - label may be included but are not used. + Parameters ---------- @@ -639,10 +638,12 @@ class EternaTidalParameters: Other columns to include in the ``data`` DataFrame. Must be array-like of the same size as the other parameters. - Returns - ------- - EternaTidalParameters - An instance of the class containing the tidal parameters. + Attributes + ---------- + data : pd.DataFrame + A DataFrame containing the tidal parameters under the columns ``freq_start``, + ``freq_stop``, ``amplitude``, and ``phase_lead``. Other columns such as a ``wave_group`` + label may be included but are not used. See Also -------- @@ -704,7 +705,7 @@ def sort_and_validate(self, gap_threshold: float | None = None) -> None: - All data are floats with no NaN. - Frequency intervals are positive and increasing - i.e. ``freq_start`` < ``freq_stop``. + i.e. ``freq_start < freq_stop``. - Frequency intervals are discrete and do not overlap. - Warn if there are gaps between successivefrequency intervals. @@ -713,7 +714,6 @@ def sort_and_validate(self, gap_threshold: float | None = None) -> None: ValueError If any of the above validations fail. """ - emsg = GSolveDataWarning(prefix=f"{type(self).__name__} error", show=True) errors = [] data_subset = self.data.loc[ @@ -799,7 +799,7 @@ def from_dataframe(cls, df: _pd.DataFrame, include_index: bool = False) -> Self: ---------- df : pd.DataFrame A DataFrame containing columns 'freq_start', 'freq_stop', 'amplitude', and - 'phase_lead'. Other columns will be included in the object's ``data`` + 'phase_lead'. Other columns will be included in the object's 'data' attribute but are not required. include_index : bool, default False If True, the index will be treated as a column when parsing the DataFrame. @@ -840,7 +840,7 @@ def from_excel(cls, fname: FilePath, sheet_name: str | int = 0) -> Self: or spreadsheet format (e.g. odf) supported by ``pandas.read_excel()``, sheet_name : str | int, default is 0 The name or index of the worksheet to read. Default is 0, the first - sheet in `fname`. + sheet in ``fname``. Returns ------- @@ -852,7 +852,6 @@ def from_excel(cls, fname: FilePath, sheet_name: str | int = 0) -> Self: object from a DataFrame. pandas.read_excel : Read an Excel file into a DataFrame. """ - df = _pd.read_excel(fname, sheet_name=sheet_name) if isinstance(df, dict): raise ValueError( @@ -863,13 +862,15 @@ def from_excel(cls, fname: FilePath, sheet_name: str | int = 0) -> Self: @classmethod def quick_tide_pro_defaults(cls) -> Self: - """Return an EternaTidalParameters instance with the - default parameters used by QuickTide Pro. + """Return an EternaTidalParameters instance with parameters used by QuickTide Pro. Quick tide pro uses the Tamura (1987) tidal potential catalogue. Using thee parameters with higer resolution tide catalogues may produce unexpected results. + Returns + ------- + EternaTidalParameters """ df = _pd.DataFrame.from_dict( data={ @@ -899,12 +900,38 @@ def default( amplitude_factor: float = 1.16, phase_lead: float = 0.0, ) -> Self: - """Return a tidal parameters object with default values used by GSolve.""" + """Return a TidalParameters object with default values used by gSolve. + + Parameters + ---------- + freq_start : float, default 0.0 + The start frequency of the tidal constituent in cycles per day (cpd). + freq_stop : float, default 10.0 + The stop frequency of the tidal constituent in cycles per day (cpd). + amplitude_factor : float, default 1.16 + The amplitude factor to apply to the tidal constituent. + phase_lead : float, default 0.0 + The phase lead in degrees to apply to the tidal constituent. + + Returns + ------- + EternaTidalParameters + An instance of the class containing the tidal parameters. + """ return cls.from_array([freq_start, freq_stop, amplitude_factor, phase_lead]) def pygtide_wavegroup_arg(self) -> NDArray[_np.float64]: - """Return a copy of the paramaters as an ndarray (shape= (N, 4)) - for use as the argument to ``pygtide.set_wavegroup()`` method. + """Return a copy of the paramaters as ndarray. + + The returned array is intended to be used as the argument to + ``pygtide.set_wavegroup()`` method. + + Returns + ------- + wavegroup : NDArray + A 2D array of shape = (n, 4) where the 4 columns correspond to + 'freq_start', 'freq_stop', 'amplitude', and 'phase_lead'. + """ return ( self.data.loc[:, ["freq_start", "freq_stop", "amplitude", "phase_lead"]] @@ -916,12 +943,6 @@ def pygtide_wavegroup_arg(self) -> NDArray[_np.float64]: class EternaPredictTidalCorrection(EarthTideCorrectionProvider): """Compute earth tide gravity corrections using PREDICT from ETERNA34. - The - - Attributes - ---------- - tidal_params : EternaTidalParameters - Parameters ---------- tidal_params : array-like | EternaTidalParameters, optional @@ -957,6 +978,10 @@ class EternaPredictTidalCorrection(EarthTideCorrectionProvider): provided by IERS, so the user should that they periodically run ``pgtide.update()`` to ensure these data are up to date. + Attributes + ---------- + tidal_params : EternaTidalParameters + See Also -------- EternaTidalParameters : Class to store tidal parameters for use with ETERNA/pygtide. @@ -1006,6 +1031,21 @@ def set_tidal_params(self, tidal_params: ArrayLike | EternaTidalParameters) -> N self._pgt.set_wavegroup(self.tidal_params.pygtide_wavegroup_arg()) def identifier(self, **kwargs) -> str: + """Return a string identifier for this corrector. + + The default identifier string includes the specified tidalpoten, + tidalcompo, amtruncate, poletidecor, and lodtidecor values. E.g. + ``EternaPredictTidalCorrection(tidalpoten=8,tidalcompo=0,...)`` + + Parameters + ---------- + kwargs : dict + Additional key-value pairs to include in the identifier string. + + Returns + ------- + str + """ if not kwargs: return self.__repr__() self._pgt_kwargs.update(kwargs) @@ -1023,10 +1063,13 @@ def time_series( unit: Literal["mgal", "ugal", "nm/s^2"] = "mgal", **kwargs, ) -> _pd.DataFrame: - """Compute a time series of tidal corrections at some location. + """ + Compute a time series of tidal corrections at some location. A limitation of ETERNA is that tides are always calculated from the start - of a UTC day. + of a UTC day with durations specified in whole hours. To ensure that the + computed time series covers the requested time interval, ``starttime`` + is rounded down to the start of the day and ``duration`` adjusted accordingly. Parameters ---------- @@ -1036,42 +1079,42 @@ def time_series( Longitude of the location in degrees. elev : float Elevation of the location in meters. - starttime : DatetimeScalar - Start - duration : TimedeltaScalar - Duration of the time series. - sample_interval : int, optional - Sampling interval in seconds (default is 60). + starttime : Timestamp, datetime-like, or str + Start of the time series in UTC + duration : Timedelta, timedelta-like, int, float or str + Duration of the time series in whole hours. Float and timedelta-like values are + rounded up to the nearest hour. + sample_interval : int, default 60 + Sampling interval in seconds. unit : {"mgal", "ugal", "nm/s^2"}, optional Unit of the output (default is "mgal"). - **kwargs : dict + kwargs : dict Additional keyword arguments to pass to the underlying pygtide predictor. Returns ------- DataFrame DataFrame containing the tidal corrections. - """ - this_run_kwargs = self._pgt_kwargs.copy() this_run_kwargs.update(kwargs) + _starttime_uncorr = to_naive_utc_datetime(starttime, allow_nat=False) + _starttime = _starttime_uncorr.normalize() - _starttime = ( - to_naive_utc_datetime(starttime, allow_nat=False) - .normalize() - .to_pydatetime() - ) - if isinstance(duration, (int, float)): - _duration = int(duration) + if isinstance(duration, int): + _duration_uncorr = duration + elif isinstance(duration, float): + _duration_uncorr = int(_np.ceil(duration)) # round up to nearest hour else: - _duration = int( + _duration_uncorr = int( _np.ceil(_pd.to_timedelta(duration).total_seconds() / 3600.0) ) - if _duration <= 0: - raise ValueError( - "duration must be a positive timedelta or number of hours." - ) + if _duration_uncorr <= 0: + raise ValueError( + "duration must be a positive timedelta or number of hours." + ) + _orig_endtime = _starttime_uncorr + _pd.Timedelta(hours=_duration_uncorr) + _duration = int(_np.ceil((_orig_endtime - _starttime).total_seconds() / 3600.0)) sample_interval = int(sample_interval) if sample_interval <= 0: @@ -1090,7 +1133,7 @@ def time_series( latitude=lat, longitude=lon, height=elev, - startdate=_starttime, + startdate=_starttime.to_pydatetime(), duration=_duration, samprate=sample_interval, **this_run_kwargs, @@ -1112,6 +1155,7 @@ def time_series( elif unit == "mgal": return df * 1e-4 + # TODO: site_id is not truly required, so remove and infer sites from lat/lon/elev def tidal_correction( self, lat: FloatArray, @@ -1123,7 +1167,42 @@ def tidal_correction( sample_interval: int = 60, **kwargs, ) -> NDArray[_np.float64]: + """Compute tidal corrections at specified locations and times. + + Parameters + ---------- + lat : array_like of float + Latitude in decimal degrees. + lon : array_like of float + Longitude in decimal degrees. + elev : array_like of float + Ellipsoidal elevation in meters. + date_time : array_like of datetime-like + Times at which to compute the tidal correction. + site_id : array_like of str + Site identifier for each correction. + unit : {"mgal", "ugal", "nm/s^2"}, default "mgal" + Tidal correction units. + sample_interval : int, default 60 + Sample interval in seconds. + kwargs : additional keyword arguments + Additional key-value pairs to be passed to the interpolation method. + Not implemented at this time. + Returns + ------- + ndarray + Tidal corrections at the specified locations and times. + + Notes + ----- + Corrections are computed by: + 1. for each unique site_id, + 2. generate a time series of tidal corrections covering the obsevarvation + times for that site, + 3. interpolate tidal corrections at the exact observation times. + + """ lat = to_1d_ndarray(lat).astype(float) lon = to_1d_ndarray(lon, expected_size=lat.size).astype(float) elev = to_1d_ndarray(elev, expected_size=lat.size).astype(float) @@ -1155,6 +1234,10 @@ def tidal_correction( # ensure at least 1 hour of data before and after the range # of date_time for this site t0 = (date_time[site_mask].min() - _pd.Timedelta(hours=1)).normalize() + + # TODO: need to break this up into multiple calls to time_series if the + # duration is too long for pygtide to handle + # e.g. sites visited days/weeks/years apart -> lots of work for nowt _duration_hrs = ( int( _np.ceil((date_time[site_mask].max() - t0).total_seconds() / 3600.0) diff --git a/src/gsolve/tide/ocean_load.py b/src/gsolve/tide/ocean_load.py index 123fa79..91348aa 100644 --- a/src/gsolve/tide/ocean_load.py +++ b/src/gsolve/tide/ocean_load.py @@ -16,9 +16,9 @@ # Copyright (c) 2025 Earth Sciences New Zealand. -import os +"""Methods and classes for reading and applying ocean load corrections to gravity data.""" + import warnings -from collections.abc import Sequence from typing import Any, Literal, Protocol, runtime_checkable import numpy as np @@ -41,8 +41,19 @@ def _read_csv_with_fallback(file_path: FilePath, **kwargs) -> pd.DataFrame: """Read a CSV trying UTF-8 (with BOM) first, then fall back to iso-8859-1. Some QuickTide Pro outputs may be UTF-8 with a BOM; others may be - iso-8859-1. Try the safe 'utf-8-sig' which strips a leading BOM, and + iso-8859-1 (latin1). Try the safe 'utf-8-sig' which strips a leading BOM, and fall back to latin1 if that fails. + + Parameters + ---------- + file_path : FilePath + The path to the CSV file to be read. + **kwargs : Any + Additional keyword arguments to pass to ``pandas.read_csv``. + + Returns + ------- + DataFrame """ try: return pd.read_csv(file_path, encoding="utf-8-sig", **kwargs) @@ -87,12 +98,12 @@ class OceanLoadAtSiteTime(OceanLoadCorrectionProvider): Site identifiers corresponding to each correction value. datetimes : DatetimeArray Sequence of datetime values corresponding to each correction value. - Must have the same length as `site_id`. + Must have the same length as ``site_id``. corrections : Sequence[float] Ocean load correction values in mGal. Must have the same length as - `site_id` and `datetimes`. + ``site_id`` and ``datetimes``. **metadata : dict[str, Any] - Additional metadata to be stored in the `obj.metadata` dictionary. + Additional metadata to be stored in the obj.metadata dictionary. Attributes ---------- @@ -141,7 +152,7 @@ def __init__( self.metadata: dict[str, Any] = metadata def identifier(self, **kwargs) -> str: - """Corrector identifier string.""" + """Corrector identifier string.""" # noqa: DOC201 return f"{type(self).__name__}()" def ocean_load_correction( @@ -154,24 +165,29 @@ def ocean_load_correction( """ Get ocean load corrections for specified site-datetime pairs. + The parameters ``site_id`` and ``date_time`` are used as pairs to look up + precalculated ocean load correction values. + Parameters ---------- site_id : array-like[str] - Site identifiers where corrections are requested. - datetime : datetime-like or array-like - Datetime values for which to get corrections. Must have the same - length as `site_id`. - if_not_matched : {"error", "warn"}, optional + Site identifiers. + date_time : array of datetime-like + Datetime values for which to get corrections. Must be the same + length as ``site_id``. + if_not_matched : {"error", "warn"}, default = "error" Action to take when site_id/datetime pairs are not found in the data. - If "error" (default), raises ValueError. If "warn", issues a warning - and returns NaN for missing values. - **kwargs : dict[str, Any] - Additional keyword arguments. (Not used). + If "error", then raise a ValueError. If "warn", issue a warning + and return NaN for unmatched pairs. + kwargs : + Additional keyword arguments. This parameter is included to maintain + compatibility with the OceanLoadCorrectionProvider interface. kwargs + are ignored. Returns ------- - np.ndarray - Array of ocean load corrections in mGal. Missing values are set to NaN. + numpy.ndarray + Array of ocean load corrections in mGal. Unmatched site_id/datetime pairs are set to NaN. """ dt = np.atleast_1d(_datetimes_to_np_datetime64(date_time)) @@ -216,26 +232,36 @@ def ocean_load_correction( class OceanLoadTimeSeries(OceanLoadCorrectionProvider): """ - A class to provide ocean load corrections at discrete times for a single location/station, - by interpolation. + Determine ocean load corrections at discrete times. + + This class provides a simple mechanism for interpolating ocean load corrections + from a pre-calculated timeseries. The class is primarily intended to be used + with timeseries produced by QuickTide Pro. Users would not typically + instantiate this class directly, but instead use the ``qtp_to_corrector()`` function + to read a QuickTide Pro output file. Parameters ---------- - datetimes : DatetimeArray + datetimes : array of datetime-like Sequence of datetime values corresponding to each correction value. - corrections : Sequence[float] + corrections : array-like of float Ocean load correction values in mGal. Must have the same length as - `datetimes`. + ``datetimes``. **metadata : dict[str, Any] - Additional metadata to be stored in the `obj.metadata` dictionary. + Additional metadata to be stored in the obj.metadata dictionary. Attributes ---------- - data : pd.DataFrame - DataFrame containing correction values, indexed by datetime. + data : pandas.DataFrame + DataFrame containing correction values, indexed and sorted by datetime. metadata : dict[str, Any] Dictionary containing metadata about the corrections. + See Also + -------- + qtp_to_corrector : Function to read QuickTide Pro output files and + create an appropriate ocean load corrector. + Examples -------- >>> # Create a generic ocean load correction provider for a single station @@ -277,7 +303,7 @@ def __repr__(self) -> str: return f"{cname}({md})" def identifier(self, **kwargs) -> str: - """Corrector identifier string.""" + """Corrector identifier string.""" # noqa: DOC201 return f"{self.__class__.__name__}()" @property @@ -302,6 +328,31 @@ def ocean_load_correction( if_not_matched: Literal["error", "warn"] = "error", **kwargs, ) -> NDArray[np.float64]: + """ + Get ocean load corrections at specified datetimes. + + Parameters + ---------- + site_id : array-like[str] + Site identifiers. This parameter is included to maintain compatibility + with the OceanLoadCorrectionProvider interface, but is ignored for this + class since it provides corrections for a single station. + date_time : array of datetime-like + Datetime values for which to get corrections. Must be within the range of + the timeseries data. + if_not_matched : {"error", "warn"}, default = "error" + Action to take when datetimes are outside the range of the timeseries data. + If "error", then raise a ValueError. If "warn", issue a warning + and return NaN for unmatched datetimes. + kwargs : + Additional keyword arguments to pass to ``numpy.interp``. + + Returns + ------- + numpy.ndarray + Array of ocean load corrections in mGal. Datetimes outside the range of + the timeseries data are set to NaN. + """ # reformat datimes to be numpy datetime64[s] ie seconds precision t = _datetimes_to_np_datetime64(date_time, dtype="datetime64[s]") df_t = self.data.index.to_numpy(dtype="datetime64[s]") @@ -333,7 +384,7 @@ def ocean_load_correction( def _datetimes_to_np_datetime64( dt: DatetimeScalar | DatetimeArray, dtype: str = "datetime64" ) -> np.ndarray: - """Convert datetimes to numpy datetime64 array.""" + """Convert datetimes to numpy datetime64 array.""" # noqa: DOC201 _dt = to_naive_utc_datetime(dt, allow_nat=False) if isinstance(_dt, pd.Timestamp): return np.array([_dt], dtype=dtype) @@ -378,6 +429,28 @@ def qtp_to_corrector( corr_type: Literal["auto", "timeseries", "site-datetime"] = "auto", metadata: dict[str, Any] | None = None, ) -> OceanLoadTimeSeries | OceanLoadAtSiteTime: + """ + Read a QuickTide Pro output file and return an appropriate ocean load corrector. + + Parameters + ---------- + file_path : FilePath + Path to the QuickTide Pro output file. + corr_type : {"auto", "timeseries", "site-datetime"}, default = "auto" + Type of correction data in the file. If "auto", the function will attempt to + determine the type based on the file contents. If "timeseries", the file is + expected to contain a single timeseries of corrections. If "site-datetime", the file is expected to contain corrections for multiple sites at specific datetimes. + metadata : dict[str, Any], optional + Additional metadata to be stored in the corrector's ``metadata`` attribute. + + Returns + ------- + OceanLoadTimeSeries or OceanLoadAtSiteTime + An instance of ``OceanLoadTimeSeries`` if the file contains a single timeseries. + An instance of ``OceanLoadAtSiteTime`` if the file contains corrections for + multiple sites at specific datetimes. + + """ if metadata is None: metadata = {} if "file_path" not in metadata: @@ -429,11 +502,9 @@ def read_qtp_timeseries(file_path: FilePath) -> pd.DataFrame: Returns ------- - pd.DataFrame + pandas.DataFrame DataFrame containing the ocean load timeseries, indexed by datetime. - """ - df = _read_csv_with_fallback(file_path, sep=r"\s+") # construct datetimes from Year, DOY, Time columns expected_columns = ["Year", "DOY", "Time"] @@ -476,8 +547,20 @@ def read_qtp_timeseries(file_path: FilePath) -> pd.DataFrame: def read_qtp_multistation(file_path: FilePath) -> pd.DataFrame: - """Read a ocean load from a CSV file.""" + """ + Read a QuickTide Pro multi-station ocean load output file. + Parameters + ---------- + file_path : str or PathLike + Path to the QuickTide Pro multi-station output file. + + Returns + ------- + pandas.DataFrame + DataFrame containing the ocean load corrections for multiple stations, + indexed by (site_id, datetime). + """ column_definitions: dict[str, type] = { "site_id": str, "datetime": str, @@ -533,25 +616,20 @@ def generate_qtp_input( Parameters ---------- - site_id : SiteIDArray + site_id : array-like of str The unique site identifier for each site/datetime pair. - datetimes : DatetimeArray + datetimes : array-like of datetime-like Datetime values for each site/datetime pair. - `site_id`. - latitude : FloatArray - The latitde for each site/datetime pair. - longitude : FloatArray + latitude : array-like of float + The latitude for each site/datetime pair. + longitude : array-like of float The longitude for each site/datetime pair. - elevation : FloatArray | float + elevation : array-like of float | float The elevation for each site/datetime pair or a single elevation value. output_file : str or PathLike Path to the output CSV file to be created. - Returns - ------- - None """ - _site_id = np.atleast_1d(site_id).astype(str) _datetimes = _datetimes_to_np_datetime64(datetimes) _lat = np.atleast_1d(latitude).astype(float) @@ -617,7 +695,7 @@ class HardispOceanLoadCorrector(OceanLoadCorrectionProvider): f : str or PathLike File containing ocean loading coefficients for a set of stations. **metadata : dict[str, Any] - Additional metadata to be stored in the `obj.metadata` dictionary. + Additional metadata to be stored in the obj.metadata dictionary. Attributes ---------- diff --git a/tests/test_core_excel_io.py b/tests/test_core_excel_io.py index 0e1e6bd..83600d9 100644 --- a/tests/test_core_excel_io.py +++ b/tests/test_core_excel_io.py @@ -18,6 +18,7 @@ from pathlib import Path +import pandas as pd import pandas.testing import pytest @@ -25,6 +26,7 @@ get_excel_worksheets, get_true_sheet_name, read_excel_worksheet, + write_excel_worksheet, ) @@ -59,3 +61,17 @@ def test_read_excel_worksheet(shared_datadir: Path) -> None: def test_get_excel_worksheets(shared_datadir: Path) -> None: f1 = shared_datadir / "legacy_format.xlsx" assert get_excel_worksheets(f1) == ["Survey Data", "Locations", "Tie Data"] + + +def test_write_excel_worksheet_accepts_excel_file_keyword(tmp_path: Path) -> None: + filename = tmp_path / "output.xlsx" + + write_excel_worksheet( + pd.DataFrame({"x": [1, 2]}), + excel_file=filename, + sheet_name="Sheet1", + index=False, + ) + + assert filename.exists() + assert get_excel_worksheets(filename) == ["Sheet1"] diff --git a/tests/test_core_utils.py b/tests/test_core_utils.py index c418b46..24954c1 100644 --- a/tests/test_core_utils.py +++ b/tests/test_core_utils.py @@ -310,7 +310,6 @@ def test_timestamp_to_columns( ymd_dataframe: pd.DataFrame, ) -> None: # Test case 1: Convert a pandas Series of timestamps to columns - # ts = ps.series(ymd_series assert_frame_equal(timestamp_to_columns(ymd_series), ymd_dataframe) # Test case: Convert datetime index to columns diff --git a/tests/test_reports.py b/tests/test_reports.py index dcb80d8..f7faed2 100644 --- a/tests/test_reports.py +++ b/tests/test_reports.py @@ -287,7 +287,7 @@ def test_to_excel_writes_terrain_sheet_when_present( sheet_names: list[str] = [] - def _fake_write_excel_worksheet(_df, _filename, sheet_name, **_kwargs): + def _fake_write_excel_worksheet(df, filename, sheet_name, **_kwargs): sheet_names.append(sheet_name) monkeypatch.setattr( diff --git a/uv.lock b/uv.lock index 7bafcb3..b4c2f55 100644 --- a/uv.lock +++ b/uv.lock @@ -564,6 +564,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] +[[package]] +name = "flake8" +version = "7.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mccabe" }, + { name = "pycodestyle" }, + { name = "pyflakes" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, +] + [[package]] name = "fonttools" version = "4.62.1" @@ -621,6 +635,7 @@ dependencies = [ { name = "boule" }, { name = "harmonica" }, { name = "pandas", extra = ["excel", "plot"] }, + { name = "pydata-sphinx-theme" }, { name = "pygtide" }, { name = "pyhardisp" }, { name = "rioxarray" }, @@ -640,6 +655,8 @@ dev = [ dev = [ { name = "burocrata" }, { name = "ipykernel" }, + { name = "ipywidgets" }, + { name = "pandas-vet" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-datadir" }, @@ -661,6 +678,7 @@ requires-dist = [ { name = "boule", specifier = "==0.6.*" }, { name = "harmonica", specifier = "<0.8" }, { name = "pandas", extras = ["excel", "plot"], specifier = "<3" }, + { name = "pydata-sphinx-theme", specifier = ">=0.16.1" }, { name = "pygtide", specifier = ">=0.8.2" }, { name = "pyhardisp", specifier = ">=0.2.1" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.3" }, @@ -677,6 +695,8 @@ provides-extras = ["dev"] dev = [ { name = "burocrata", specifier = ">=0.3.0" }, { name = "ipykernel", specifier = ">=7.2.0" }, + { name = "ipywidgets", specifier = ">=8.1.8" }, + { name = "pandas-vet", specifier = ">=2023.8.2" }, { name = "pytest", specifier = ">=9.0.3" }, { name = "pytest-cov", specifier = ">=7.1.0" }, { name = "pytest-datadir", specifier = ">=1.8.0" }, @@ -798,6 +818,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, ] +[[package]] +name = "ipywidgets" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "comm" }, + { name = "ipython" }, + { name = "jupyterlab-widgets" }, + { name = "traitlets" }, + { name = "widgetsnbextension" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/ae/c5ce1edc1afe042eadb445e95b0671b03cee61895264357956e61c0d2ac0/ipywidgets-8.1.8.tar.gz", hash = "sha256:61f969306b95f85fba6b6986b7fe45d73124d1d9e3023a8068710d47a22ea668", size = 116739, upload-time = "2025-11-01T21:18:12.393Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl", hash = "sha256:ecaca67aed704a338f88f67b1181b58f821ab5dc89c1f0f5ef99db43c1c2921e", size = 139808, upload-time = "2025-11-01T21:18:10.956Z" }, +] + [[package]] name = "jedi" version = "0.20.0" @@ -860,6 +896,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, ] +[[package]] +name = "jupyterlab-widgets" +version = "3.0.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/2d/ef58fed122b268c69c0aa099da20bc67657cdfb2e222688d5731bd5b971d/jupyterlab_widgets-3.0.16.tar.gz", hash = "sha256:423da05071d55cf27a9e602216d35a3a65a3e41cdf9c5d3b643b814ce38c19e0", size = 897423, upload-time = "2025-11-01T21:11:29.724Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl", hash = "sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8", size = 914926, upload-time = "2025-11-01T21:11:28.008Z" }, +] + [[package]] name = "kiwisolver" version = "1.5.0" @@ -1120,6 +1165,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, ] +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + [[package]] name = "mdit-py-plugins" version = "0.5.0" @@ -1358,6 +1412,19 @@ plot = [ { name = "matplotlib" }, ] +[[package]] +name = "pandas-vet" +version = "2023.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "flake8" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/57/2c8350ff48cece1f6009012a0f42413e9e6d5ca54bec7f3b5726b6e33e1b/pandas_vet-2023.8.2.tar.gz", hash = "sha256:ee6b80b40f5a74de41c29393e4dce373447b363faaa3e0677e4f36799ea865c3", size = 9029, upload-time = "2023-08-11T15:09:03.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/dd/128ade78c67d4ac1a6202f320112aaa7934eaf8f7b0d861517e7c960dd3f/pandas_vet-2023.8.2-py3-none-any.whl", hash = "sha256:761c07b4479f24fc507465047111c7d21374ec678b7aba58e454fd5d3f8055fe", size = 7396, upload-time = "2023-08-11T15:09:04.842Z" }, +] + [[package]] name = "parso" version = "0.8.7" @@ -1560,6 +1627,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, ] +[[package]] +name = "pycodestyle" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -1587,6 +1663,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e2/0d/8ba33fa83a7dcde13eb3c1c2a0c1cc29950a048bfed6d9b0d8b6bd710b4c/pydata_sphinx_theme-0.16.1-py3-none-any.whl", hash = "sha256:225331e8ac4b32682c18fcac5a57a6f717c4e632cea5dd0e247b55155faeccde", size = 6723264, upload-time = "2024-12-17T10:53:35.645Z" }, ] +[[package]] +name = "pyflakes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -2491,6 +2576,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2", size = 110825, upload-time = "2026-05-02T16:04:11.033Z" }, ] +[[package]] +name = "widgetsnbextension" +version = "4.0.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/f4/c67440c7fb409a71b7404b7aefcd7569a9c0d6bd071299bf4198ae7a5d95/widgetsnbextension-4.0.15.tar.gz", hash = "sha256:de8610639996f1567952d763a5a41af8af37f2575a41f9852a38f947eb82a3b9", size = 1097402, upload-time = "2025-11-01T21:15:55.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl", hash = "sha256:8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366", size = 2196503, upload-time = "2025-11-01T21:15:53.565Z" }, +] + [[package]] name = "xarray" version = "2026.4.0"