mostlyright-data 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mostlyright/data_harness/__init__.py +158 -0
- mostlyright/data_harness/acquisition/__init__.py +55 -0
- mostlyright/data_harness/acquisition/http.py +2773 -0
- mostlyright/data_harness/acquisition/parsing.py +809 -0
- mostlyright/data_harness/acquisition/ranges.py +495 -0
- mostlyright/data_harness/acquisition/result_download.py +360 -0
- mostlyright/data_harness/acquisition/retention_admission.py +248 -0
- mostlyright/data_harness/acquisition/sandbox.py +4888 -0
- mostlyright/data_harness/acquisition/url_policy.py +530 -0
- mostlyright/data_harness/agent_runtime.py +2743 -0
- mostlyright/data_harness/assets/logo-ink.svg +31 -0
- mostlyright/data_harness/backends/__init__.py +28 -0
- mostlyright/data_harness/backends/pandas_backend.py +350 -0
- mostlyright/data_harness/backends/polars_backend.py +366 -0
- mostlyright/data_harness/backends/protocol.py +124 -0
- mostlyright/data_harness/backends/reference.py +83 -0
- mostlyright/data_harness/backends/registry.py +55 -0
- mostlyright/data_harness/backends/restrictions.py +126 -0
- mostlyright/data_harness/canonical.py +333 -0
- mostlyright/data_harness/catalog_job.py +625 -0
- mostlyright/data_harness/cli.py +5398 -0
- mostlyright/data_harness/contracts.py +53 -0
- mostlyright/data_harness/coordinator.py +1307 -0
- mostlyright/data_harness/deploy.py +924 -0
- mostlyright/data_harness/deploy_target.py +312 -0
- mostlyright/data_harness/deployment_evidence.py +1067 -0
- mostlyright/data_harness/event_presentation.py +576 -0
- mostlyright/data_harness/events.py +2152 -0
- mostlyright/data_harness/fast_delimited.py +239 -0
- mostlyright/data_harness/fleet.py +237 -0
- mostlyright/data_harness/formats.py +236 -0
- mostlyright/data_harness/governors.py +1163 -0
- mostlyright/data_harness/hosted_bootstrap.py +972 -0
- mostlyright/data_harness/hosted_crawler.py +1115 -0
- mostlyright/data_harness/hosted_crawler_container_smoke.py +351 -0
- mostlyright/data_harness/hosted_crawler_fetch.py +423 -0
- mostlyright/data_harness/hosted_crawler_job.py +1277 -0
- mostlyright/data_harness/hosted_crawler_protocol.py +676 -0
- mostlyright/data_harness/hosted_dataset.py +1500 -0
- mostlyright/data_harness/hosted_deploy.py +3037 -0
- mostlyright/data_harness/hosted_handoff.py +62 -0
- mostlyright/data_harness/hosted_ingestion_contract.py +504 -0
- mostlyright/data_harness/hosted_ingestion_job.py +356 -0
- mostlyright/data_harness/hosted_ingestion_job_smoke.py +40 -0
- mostlyright/data_harness/hosted_session_container_smoke.py +194 -0
- mostlyright/data_harness/hosted_session_worker.py +3554 -0
- mostlyright/data_harness/hosted_session_worker_job_smoke.py +46 -0
- mostlyright/data_harness/hosted_worker.py +6784 -0
- mostlyright/data_harness/ingestion/__init__.py +56 -0
- mostlyright/data_harness/ingestion/contracts.py +461 -0
- mostlyright/data_harness/ingestion/faults.py +42 -0
- mostlyright/data_harness/ingestion/gcs_store.py +1162 -0
- mostlyright/data_harness/ingestion/spool.py +130 -0
- mostlyright/data_harness/ingestion/store.py +885 -0
- mostlyright/data_harness/key_seam.py +434 -0
- mostlyright/data_harness/linux_process_boundary.py +262 -0
- mostlyright/data_harness/local_contracts.py +2880 -0
- mostlyright/data_harness/local_search/__init__.py +5 -0
- mostlyright/data_harness/local_search/build_index.py +1087 -0
- mostlyright/data_harness/local_search/contracts.py +920 -0
- mostlyright/data_harness/local_search/query_trace.py +266 -0
- mostlyright/data_harness/local_search/retrieval.py +700 -0
- mostlyright/data_harness/local_search/sealed.py +474 -0
- mostlyright/data_harness/local_search/service.py +784 -0
- mostlyright/data_harness/nbrender/CONTRACT.md +212 -0
- mostlyright/data_harness/nbrender/__init__.py +12 -0
- mostlyright/data_harness/nbrender/chrome.py +359 -0
- mostlyright/data_harness/nbrender/code_body.py +266 -0
- mostlyright/data_harness/nbrender/document.py +407 -0
- mostlyright/data_harness/nbrender/frame.py +275 -0
- mostlyright/data_harness/nbrender/interactive.py +337 -0
- mostlyright/data_harness/nbrender/markdown_body.py +477 -0
- mostlyright/data_harness/nbrender/mr_components.py +134 -0
- mostlyright/data_harness/nbrender/outputs_data.py +595 -0
- mostlyright/data_harness/nbrender/outputs_rich.py +906 -0
- mostlyright/data_harness/nbrender/outputs_source.py +260 -0
- mostlyright/data_harness/nbrender/outputs_stage.py +176 -0
- mostlyright/data_harness/nbrender/outputs_text.py +400 -0
- mostlyright/data_harness/nbrender/parse.py +394 -0
- mostlyright/data_harness/nbrender/status.py +40 -0
- mostlyright/data_harness/nbrender/tokens.py +1295 -0
- mostlyright/data_harness/notebook.py +1710 -0
- mostlyright/data_harness/offline.py +2049 -0
- mostlyright/data_harness/operation_registry.py +1007 -0
- mostlyright/data_harness/operator_setup.py +239 -0
- mostlyright/data_harness/pipeline.py +6428 -0
- mostlyright/data_harness/plan_graph.py +2026 -0
- mostlyright/data_harness/preparation/__init__.py +104 -0
- mostlyright/data_harness/preparation/contracts.py +1017 -0
- mostlyright/data_harness/preparation/engine.py +221 -0
- mostlyright/data_harness/preparation/errors.py +14 -0
- mostlyright/data_harness/preparation/gates.py +751 -0
- mostlyright/data_harness/preparation/joins.py +574 -0
- mostlyright/data_harness/preparation/profile.py +384 -0
- mostlyright/data_harness/preparation/table.py +217 -0
- mostlyright/data_harness/preparation/transforms.py +568 -0
- mostlyright/data_harness/progress_events.py +534 -0
- mostlyright/data_harness/readers/__init__.py +46 -0
- mostlyright/data_harness/readers/containers.py +963 -0
- mostlyright/data_harness/readers/contracts.py +542 -0
- mostlyright/data_harness/readers/delimited.py +257 -0
- mostlyright/data_harness/readers/grib2/__init__.py +33 -0
- mostlyright/data_harness/readers/grib2/admission.py +722 -0
- mostlyright/data_harness/readers/grib2/decode.py +1009 -0
- mostlyright/data_harness/readers/grib2/geometry.py +1133 -0
- mostlyright/data_harness/readers/grib2/portable_math.py +501 -0
- mostlyright/data_harness/readers/json_tabular.py +485 -0
- mostlyright/data_harness/readers/registry.py +514 -0
- mostlyright/data_harness/readers/samples/README.md +110 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/cities.csv.gz +0 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/cities.csv.gz +0 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/cities.tar +0 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/cities.tar +0 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/cities.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/cities.csv +3 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/expected.json +23 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/cities.tsv +5 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/expected.json +25 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/expected.json +30 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/observations.csv +5 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/expected.json +44 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/stations.json +1 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/expected.json +48 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/observations.ndjson +4 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/cities.xlsx +0 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/expected.json +24 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/expected.json +27 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/expected.json +27 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/README.md +20 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/expected.json +55 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/gfs-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/hrrr-categorical-rain.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples.py +582 -0
- mostlyright/data_harness/readers/spreadsheet.py +803 -0
- mostlyright/data_harness/readers/tabular.py +510 -0
- mostlyright/data_harness/recipe.py +5321 -0
- mostlyright/data_harness/repair/__init__.py +78 -0
- mostlyright/data_harness/repair/adapters.py +274 -0
- mostlyright/data_harness/repair/contracts.py +872 -0
- mostlyright/data_harness/repair/coordinator.py +1099 -0
- mostlyright/data_harness/repair/errors.py +16 -0
- mostlyright/data_harness/review.py +2533 -0
- mostlyright/data_harness/rowset.py +283 -0
- mostlyright/data_harness/serving.py +1975 -0
- mostlyright/data_harness/serving_edge.py +590 -0
- mostlyright/data_harness/serving_http.py +1031 -0
- mostlyright/data_harness/session_probes.py +759 -0
- mostlyright/data_harness/signing.py +101 -0
- mostlyright/data_harness/source_discovery.py +898 -0
- mostlyright/data_harness/sources/__init__.py +209 -0
- mostlyright/data_harness/sources/_adapter_steps.py +213 -0
- mostlyright/data_harness/sources/adapters.py +1214 -0
- mostlyright/data_harness/sources/cadence.py +1428 -0
- mostlyright/data_harness/sources/cadence_emission.py +453 -0
- mostlyright/data_harness/sources/cadence_history.py +546 -0
- mostlyright/data_harness/sources/catalog/__init__.py +17 -0
- mostlyright/data_harness/sources/catalog/admission.py +477 -0
- mostlyright/data_harness/sources/catalog/authoring.py +1701 -0
- mostlyright/data_harness/sources/catalog/authoring_policy.py +701 -0
- mostlyright/data_harness/sources/catalog/authoring_shards.py +1217 -0
- mostlyright/data_harness/sources/catalog/bounded_io.py +231 -0
- mostlyright/data_harness/sources/catalog/channel.py +523 -0
- mostlyright/data_harness/sources/catalog/channel_client.py +296 -0
- mostlyright/data_harness/sources/catalog/contracts.py +825 -0
- mostlyright/data_harness/sources/catalog/coverage.py +137 -0
- mostlyright/data_harness/sources/catalog/delta.py +1340 -0
- mostlyright/data_harness/sources/catalog/embedding.py +532 -0
- mostlyright/data_harness/sources/catalog/entry_v2.py +1182 -0
- mostlyright/data_harness/sources/catalog/fill.py +3889 -0
- mostlyright/data_harness/sources/catalog/fill_partitions.py +459 -0
- mostlyright/data_harness/sources/catalog/fill_staging.py +1105 -0
- mostlyright/data_harness/sources/catalog/gating.py +374 -0
- mostlyright/data_harness/sources/catalog/generation_receipt.py +1607 -0
- mostlyright/data_harness/sources/catalog/harvest/__init__.py +7 -0
- mostlyright/data_harness/sources/catalog/harvest/ckan.py +384 -0
- mostlyright/data_harness/sources/catalog/harvest/datagov_v4.py +798 -0
- mostlyright/data_harness/sources/catalog/harvest/protocol.py +964 -0
- mostlyright/data_harness/sources/catalog/harvest/sdmx.py +445 -0
- mostlyright/data_harness/sources/catalog/harvest/stac.py +384 -0
- mostlyright/data_harness/sources/catalog/health.py +447 -0
- mostlyright/data_harness/sources/catalog/hosted_catalog.py +105 -0
- mostlyright/data_harness/sources/catalog/identity_history.py +1549 -0
- mostlyright/data_harness/sources/catalog/neural.py +1618 -0
- mostlyright/data_harness/sources/catalog/packed_catalog.py +2345 -0
- mostlyright/data_harness/sources/catalog/packed_retrieval.py +1517 -0
- mostlyright/data_harness/sources/catalog/packed_writer.py +2802 -0
- mostlyright/data_harness/sources/catalog/query_trace.py +1037 -0
- mostlyright/data_harness/sources/catalog/recommend.py +171 -0
- mostlyright/data_harness/sources/catalog/retrieval.py +230 -0
- mostlyright/data_harness/sources/catalog/retrieval_manifest.py +995 -0
- mostlyright/data_harness/sources/catalog/rights_decisions.py +254 -0
- mostlyright/data_harness/sources/catalog/sealed.py +560 -0
- mostlyright/data_harness/sources/catalog/search.py +230 -0
- mostlyright/data_harness/sources/catalog/streaming_delta.py +1097 -0
- mostlyright/data_harness/sources/catalog/update.py +891 -0
- mostlyright/data_harness/sources/collections.py +815 -0
- mostlyright/data_harness/sources/contracts.py +2223 -0
- mostlyright/data_harness/sources/deletion.py +761 -0
- mostlyright/data_harness/sources/fitness.py +162 -0
- mostlyright/data_harness/sources/governance.py +163 -0
- mostlyright/data_harness/sources/hosted.py +173 -0
- mostlyright/data_harness/sources/integration.py +218 -0
- mostlyright/data_harness/sources/range_reader.py +418 -0
- mostlyright/data_harness/sources/registry.py +514 -0
- mostlyright/data_harness/sources/rights_rule.py +59 -0
- mostlyright/data_harness/sources/source_cadence_vectors.v1.json +1 -0
- mostlyright/data_harness/sources/sports.py +521 -0
- mostlyright/data_harness/sources/stream.py +524 -0
- mostlyright/data_harness/sources/stream_connector.py +418 -0
- mostlyright/data_harness/sources/stream_recorder.py +1404 -0
- mostlyright/data_harness/studio_boundary.py +2019 -0
- mostlyright/data_harness/thin/__init__.py +37 -0
- mostlyright/data_harness/thin/acquire.py +1137 -0
- mostlyright/data_harness/thin/acquire_cancel.py +579 -0
- mostlyright/data_harness/thin/approvals.py +617 -0
- mostlyright/data_harness/thin/commands.py +406 -0
- mostlyright/data_harness/thin/download.py +194 -0
- mostlyright/data_harness/thin/narrative.py +589 -0
- mostlyright/data_harness/thin/parity.py +1070 -0
- mostlyright/data_harness/thin/propose.py +2759 -0
- mostlyright/data_harness/thin/research.py +1663 -0
- mostlyright/data_harness/thin/router.py +924 -0
- mostlyright/data_harness/thin/runs.py +519 -0
- mostlyright/data_harness/thin/session.py +281 -0
- mostlyright/data_harness/thin/stream.py +501 -0
- mostlyright/data_harness/thin/transport.py +187 -0
- mostlyright/data_harness/thin/vocabulary.py +368 -0
- mostlyright/data_harness/thin/workers.py +164 -0
- mostlyright/data_harness/ucum/TABLE-PIN.json +40 -0
- mostlyright/data_harness/ucum/ucum-subset.v1.json +632 -0
- mostlyright/data_harness/unit_flow.py +927 -0
- mostlyright/data_harness/units.py +572 -0
- mostlyright/data_harness/ux/__init__.py +9 -0
- mostlyright/data_harness/ux/approve.py +485 -0
- mostlyright/data_harness/ux/author_yaml.py +597 -0
- mostlyright/data_harness/ux/cloud_auth.py +447 -0
- mostlyright/data_harness/ux/commands/__init__.py +260 -0
- mostlyright/data_harness/ux/commands/approve.py +136 -0
- mostlyright/data_harness/ux/commands/auth.py +744 -0
- mostlyright/data_harness/ux/commands/author.py +79 -0
- mostlyright/data_harness/ux/commands/catalog_author.py +403 -0
- mostlyright/data_harness/ux/commands/catalog_fill.py +523 -0
- mostlyright/data_harness/ux/commands/catalog_harvest.py +545 -0
- mostlyright/data_harness/ux/commands/catalog_publish.py +1838 -0
- mostlyright/data_harness/ux/commands/catalog_search.py +71 -0
- mostlyright/data_harness/ux/commands/catalog_update.py +437 -0
- mostlyright/data_harness/ux/commands/deploy.py +134 -0
- mostlyright/data_harness/ux/commands/deploy_dataset.py +98 -0
- mostlyright/data_harness/ux/commands/deploy_plan.py +105 -0
- mostlyright/data_harness/ux/commands/deploy_status.py +104 -0
- mostlyright/data_harness/ux/commands/diff.py +74 -0
- mostlyright/data_harness/ux/commands/index.py +84 -0
- mostlyright/data_harness/ux/commands/inventory.py +47 -0
- mostlyright/data_harness/ux/commands/list_builds.py +143 -0
- mostlyright/data_harness/ux/commands/login.py +63 -0
- mostlyright/data_harness/ux/commands/peek.py +236 -0
- mostlyright/data_harness/ux/commands/plan_check.py +90 -0
- mostlyright/data_harness/ux/commands/preflight.py +97 -0
- mostlyright/data_harness/ux/commands/record.py +107 -0
- mostlyright/data_harness/ux/commands/review_setup.py +47 -0
- mostlyright/data_harness/ux/commands/search.py +440 -0
- mostlyright/data_harness/ux/commands/show.py +61 -0
- mostlyright/data_harness/ux/commands/whoami.py +37 -0
- mostlyright/data_harness/ux/credential_native.py +551 -0
- mostlyright/data_harness/ux/credential_store.py +1055 -0
- mostlyright/data_harness/ux/credentials.py +631 -0
- mostlyright/data_harness/ux/diffing.py +444 -0
- mostlyright/data_harness/ux/headline.py +671 -0
- mostlyright/data_harness/ux/hosted_acquisition.py +974 -0
- mostlyright/data_harness/ux/hosted_run_status.py +619 -0
- mostlyright/data_harness/ux/inventory.py +427 -0
- mostlyright/data_harness/ux/local_review.py +375 -0
- mostlyright/data_harness/ux/login.py +691 -0
- mostlyright/data_harness/ux/path_kind.py +147 -0
- mostlyright/data_harness/ux/peek.py +1000 -0
- mostlyright/data_harness/ux/plain_file.py +178 -0
- mostlyright/data_harness/ux/plan_check.py +311 -0
- mostlyright/data_harness/ux/preflight.py +918 -0
- mostlyright/data_harness/ux/readers.py +1124 -0
- mostlyright/data_harness/ux/remediation.py +2195 -0
- mostlyright/data_harness/ux/render.py +657 -0
- mostlyright/data_harness/ux/workload.py +1077 -0
- mostlyright/data_harness/viewer.py +3713 -0
- mostlyright/data_harness/visual_run/__init__.py +83 -0
- mostlyright/data_harness/visual_run/authoring.py +235 -0
- mostlyright/data_harness/visual_run/contracts.py +673 -0
- mostlyright/data_harness/visual_run/materialize.py +486 -0
- mostlyright/data_harness/visual_run/observations.py +874 -0
- mostlyright/data_harness/visual_run/query.py +259 -0
- mostlyright/data_harness/visual_run/reducer.py +280 -0
- mostlyright/data_harness/visual_run/sdk.py +892 -0
- mostlyright/data_harness/visual_run/store.py +584 -0
- mostlyright/data_harness/visual_run/transport.py +239 -0
- mostlyright/data_harness/watch.py +2999 -0
- mostlyright_data-0.9.0.dist-info/METADATA +607 -0
- mostlyright_data-0.9.0.dist-info/RECORD +314 -0
- mostlyright_data-0.9.0.dist-info/WHEEL +4 -0
- mostlyright_data-0.9.0.dist-info/entry_points.txt +12 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
"""Trigonometry that answers the same on every machine, computed here rather than asked of C.
|
|
2
|
+
|
|
3
|
+
Why this module exists
|
|
4
|
+
----------------------
|
|
5
|
+
IEEE-754 requires addition, subtraction, multiplication, division and square root to be
|
|
6
|
+
correctly rounded: two conforming machines given the same doubles hand back the same double,
|
|
7
|
+
bit for bit. It requires nothing of the kind for sine, cosine, tangent, logarithm, arc tangent
|
|
8
|
+
or raising to a fractional power. Every C library approximates those its own way, and two of
|
|
9
|
+
them may differ in the last place with neither of them wrong.
|
|
10
|
+
|
|
11
|
+
That is not a theoretical difference here. The flagship weather model declares both of its
|
|
12
|
+
standard parallels at 38.5 degrees, so the whole cone constant of its projection is one sine::
|
|
13
|
+
|
|
14
|
+
math.sin(math.radians(38.5))
|
|
15
|
+
|
|
16
|
+
glibc on x86_64 0x1.3eba3d0b47a0ep-1
|
|
17
|
+
macOS on arm64 0x1.3eba3d0b47a0fp-1
|
|
18
|
+
|
|
19
|
+
One unit in the last place. Carried through the projection it moved the reported latitude of a
|
|
20
|
+
grid cell over Chicago from ``41.87656533464987`` to ``41.87656533464984``, which moved the
|
|
21
|
+
digest of a known-good sample, which failed the sample gate on one architecture and passed it on
|
|
22
|
+
the other. A dataset this harness builds is meant to be reproducible from its recipe and its
|
|
23
|
+
bytes; a coordinate that also depends on which C library the machine was built against is not
|
|
24
|
+
that, and it fails in the quiet way: the number verifies, it plots, and it disagrees with the
|
|
25
|
+
same number computed next door.
|
|
26
|
+
|
|
27
|
+
What this module guarantees, and what it does not
|
|
28
|
+
-------------------------------------------------
|
|
29
|
+
**Guaranteed: one float64 argument produces one float64 answer, the same on every machine.**
|
|
30
|
+
Every step between them is a decimal operation the decimal arithmetic specification requires to
|
|
31
|
+
be correctly rounded -- add, subtract, multiply, divide, square root -- carried at a fixed
|
|
32
|
+
precision, in a fixed order, from constants this module computes for itself. Nothing on that
|
|
33
|
+
path is left to the machine.
|
|
34
|
+
|
|
35
|
+
**Not claimed: correct rounding.** Each series below stops when a further term no longer moves
|
|
36
|
+
the running total at fifty significant digits, and the result is rounded to float64 once, at the
|
|
37
|
+
end. Claiming correct rounding would need a proof that no true value here lands within
|
|
38
|
+
10^-33 of the midpoint between two doubles, and no such proof is offered. What is offered is
|
|
39
|
+
that whatever answer comes out, the same answer comes out everywhere, which is the property a
|
|
40
|
+
reproducible receipt actually needs.
|
|
41
|
+
|
|
42
|
+
**Accuracy is graded rather than asserted.** The tests compare every function here against the
|
|
43
|
+
platform's own C library across the range this Reader uses, and against the identities that
|
|
44
|
+
relate them. Beyond that, the projection built on this module is compared against the
|
|
45
|
+
publisher's own declared grid corner on every message read -- so an error here does not wait for
|
|
46
|
+
a test to be noticed, it stops a decode.
|
|
47
|
+
|
|
48
|
+
What is not computed in decimal, and why
|
|
49
|
+
----------------------------------------
|
|
50
|
+
``PI``, ``radians`` and ``degrees`` are exact float64 work: a constant, and one multiplication
|
|
51
|
+
by a factor derived with one correctly rounded division. They live here so the projection has a
|
|
52
|
+
single place to reach for its arithmetic, not because they were ever in doubt. A correctly
|
|
53
|
+
rounded multiplication is a correctly rounded multiplication everywhere, and running them
|
|
54
|
+
through decimal would cost time to buy nothing.
|
|
55
|
+
|
|
56
|
+
The method, in the order the code runs it
|
|
57
|
+
-----------------------------------------
|
|
58
|
+
1. The float64 argument converts to ``Decimal`` exactly. A binary64 value is a terminating
|
|
59
|
+
decimal, so this step loses nothing and invents nothing.
|
|
60
|
+
2. The argument is reduced to the interval where its series converges quickly: sine and cosine
|
|
61
|
+
modulo a quarter turn, the logarithm to a mantissa near one, the exponential modulo the
|
|
62
|
+
logarithm of ten, the arc tangent by repeated halving. Reduction uses constants carried to
|
|
63
|
+
seventy-five digits so the subtraction that does it cannot lose the digits that matter.
|
|
64
|
+
3. The reduced argument goes into a plain series, summed until a term no longer changes the
|
|
65
|
+
total at the working precision.
|
|
66
|
+
4. The result is rounded to float64 exactly once, on the way out.
|
|
67
|
+
|
|
68
|
+
The constants
|
|
69
|
+
-------------
|
|
70
|
+
``pi`` comes from Machin's formula, ``pi = 16 atan(1/5) - 4 atan(1/239)``; the natural
|
|
71
|
+
logarithms of two and ten come from the ``atanh`` series. They are computed at import from
|
|
72
|
+
those formulae rather than pasted in as digits, so there is no transcription to get wrong. The
|
|
73
|
+
tests compare their leading digits against published values, which is the check a pasted
|
|
74
|
+
constant would have needed anyway.
|
|
75
|
+
|
|
76
|
+
Import discipline: ``decimal`` and nothing else, ever. A module whose entire purpose is to
|
|
77
|
+
stop depending on the machine's arithmetic has no business importing anything that might.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
from __future__ import annotations
|
|
81
|
+
|
|
82
|
+
import decimal
|
|
83
|
+
from decimal import Decimal
|
|
84
|
+
|
|
85
|
+
__all__ = [
|
|
86
|
+
"CONSTANT_DIGITS",
|
|
87
|
+
"PI",
|
|
88
|
+
"WORKING_DIGITS",
|
|
89
|
+
"atan",
|
|
90
|
+
"atan2",
|
|
91
|
+
"cos",
|
|
92
|
+
"degrees",
|
|
93
|
+
"hypot",
|
|
94
|
+
"log",
|
|
95
|
+
"power",
|
|
96
|
+
"radians",
|
|
97
|
+
"sin",
|
|
98
|
+
"tan",
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
# Fifty digits to work in, seventy-five to reduce with. A float64 needs seventeen to be written
|
|
102
|
+
# down exactly, so the working precision carries thirty-three digits of headroom over what the
|
|
103
|
+
# answer can express, and the reduction carries twenty-five more than that so the cancelling
|
|
104
|
+
# subtraction inside it cannot eat the digits the series then works from.
|
|
105
|
+
WORKING_DIGITS = 50
|
|
106
|
+
CONSTANT_DIGITS = 75
|
|
107
|
+
|
|
108
|
+
# The reduction subtracts a multiple of a constant known to CONSTANT_DIGITS digits, so a very
|
|
109
|
+
# large argument would spend that headroom on its integer part. Nothing on this Reader's path
|
|
110
|
+
# reaches an angle of a thousand million radians -- every one of them is bounded by half a turn
|
|
111
|
+
# or by a latitude -- so the bound is stated and enforced rather than assumed.
|
|
112
|
+
_LARGEST_REDUCIBLE_ANGLE = Decimal(10) ** 10
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _context(digits: int) -> decimal.Context:
|
|
116
|
+
"""A context that decides everything: precision, rounding, range, and what is fatal.
|
|
117
|
+
|
|
118
|
+
Constructed rather than inherited. A caller who has set a precision of five digits and
|
|
119
|
+
rounding towards zero must not be able to change this module's answer, and a caller's flags
|
|
120
|
+
must not be disturbed by having called it, so every function here runs inside one of these
|
|
121
|
+
and never inside whatever context it was called from.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
return decimal.Context(
|
|
125
|
+
prec=digits,
|
|
126
|
+
rounding=decimal.ROUND_HALF_EVEN,
|
|
127
|
+
Emin=-999999999,
|
|
128
|
+
Emax=999999999,
|
|
129
|
+
capitals=1,
|
|
130
|
+
clamp=0,
|
|
131
|
+
flags=[],
|
|
132
|
+
# An operation with no answer, and a division by zero, are faults to raise rather than
|
|
133
|
+
# quiet NaNs to propagate. Overflow is deliberately not fatal: it becomes an infinity,
|
|
134
|
+
# which is what float64 arithmetic would have produced at the same point.
|
|
135
|
+
traps=[decimal.InvalidOperation, decimal.DivisionByZero],
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
_WORKING = _context(WORKING_DIGITS)
|
|
140
|
+
_REDUCING = _context(CONSTANT_DIGITS)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# --- The constants, computed from their own formulae ----------------------------------------
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _arc_tangent_of_reciprocal(whole: int) -> Decimal:
|
|
147
|
+
"""``atan(1/whole)`` by its alternating series, for whole numbers of two and up.
|
|
148
|
+
|
|
149
|
+
Used only to build pi, where the two reciprocals are a fifth and a two hundred and
|
|
150
|
+
thirty-ninth and the series converges in a few dozen terms.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
x = Decimal(1) / Decimal(whole)
|
|
154
|
+
square = x * x
|
|
155
|
+
signed = x
|
|
156
|
+
total = x
|
|
157
|
+
term = 1
|
|
158
|
+
while True:
|
|
159
|
+
signed = -signed * square
|
|
160
|
+
addend = signed / (2 * term + 1)
|
|
161
|
+
if total + addend == total:
|
|
162
|
+
return total
|
|
163
|
+
total += addend
|
|
164
|
+
term += 1
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _logarithm_series(mantissa: Decimal) -> Decimal:
|
|
168
|
+
"""``log(mantissa)`` by the ``atanh`` series, for a mantissa the caller has already reduced.
|
|
169
|
+
|
|
170
|
+
``log(m) = 2 (z + z^3/3 + z^5/5 + ...)`` with ``z = (m - 1) / (m + 1)``. Every caller here
|
|
171
|
+
hands over a mantissa between the square roots of a half and of two, where ``|z|`` is under
|
|
172
|
+
0.18 and the series settles in about forty terms.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
z = (mantissa - 1) / (mantissa + 1)
|
|
176
|
+
square = z * z
|
|
177
|
+
power_of_z = z
|
|
178
|
+
total = z
|
|
179
|
+
term = 1
|
|
180
|
+
while True:
|
|
181
|
+
power_of_z = power_of_z * square
|
|
182
|
+
addend = power_of_z / (2 * term + 1)
|
|
183
|
+
if total + addend == total:
|
|
184
|
+
return 2 * total
|
|
185
|
+
total += addend
|
|
186
|
+
term += 1
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
with decimal.localcontext(_REDUCING):
|
|
190
|
+
_PI = 16 * _arc_tangent_of_reciprocal(5) - 4 * _arc_tangent_of_reciprocal(239)
|
|
191
|
+
_HALF_PI = _PI / 2
|
|
192
|
+
_LOG_TWO = _logarithm_series(Decimal(2))
|
|
193
|
+
# ten is eight times a quarter more than one, so the logarithm of ten is three logarithms of
|
|
194
|
+
# two plus one more series, all of them reduced enough to converge quickly.
|
|
195
|
+
_LOG_TEN = 3 * _LOG_TWO + _logarithm_series(Decimal("1.25"))
|
|
196
|
+
_ROOT_TWO = Decimal(2).sqrt()
|
|
197
|
+
|
|
198
|
+
#: Pi as a float64. Equal to ``math.pi``, and the tests assert that it is: this module computes
|
|
199
|
+
#: the constant it needs rather than importing one, and the agreement is the evidence that both
|
|
200
|
+
#: are the correctly rounded double.
|
|
201
|
+
PI = float(_PI)
|
|
202
|
+
|
|
203
|
+
#: One degree in radians, and one radian in degrees. Each is a single IEEE-754 division of two
|
|
204
|
+
#: doubles, which every conforming machine rounds the same way, so a conversion is one exactly
|
|
205
|
+
#: reproducible multiplication.
|
|
206
|
+
_DEGREES_TO_RADIANS = PI / 180.0
|
|
207
|
+
_RADIANS_TO_DEGREES = 180.0 / PI
|
|
208
|
+
|
|
209
|
+
# The arc tangent series is halved down to here before it is summed. Below a twentieth the
|
|
210
|
+
# terms fall by a factor of four hundred each time and thirty of them are more than enough.
|
|
211
|
+
_ARC_TANGENT_THRESHOLD = Decimal("0.05")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# --- What a caller may hand in ---------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _argument(value: float, function: str) -> Decimal:
|
|
218
|
+
"""One argument, exactly, or a refusal that names the function and what arrived.
|
|
219
|
+
|
|
220
|
+
A bool is a whole number to Python and an argument nobody meant to pass here, so it is
|
|
221
|
+
turned away with everything else that is not a real number.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
225
|
+
raise TypeError(f"{function} takes a real number, not {value!r}")
|
|
226
|
+
number = Decimal(value)
|
|
227
|
+
if not number.is_finite():
|
|
228
|
+
raise ValueError(f"{function} takes a finite number, not {value!r}")
|
|
229
|
+
return number
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# --- Sine, cosine and tangent ----------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _quarter_turns(angle: Decimal) -> tuple[int, Decimal]:
|
|
236
|
+
"""An angle as a whole number of quarter turns and a remainder of at most an eighth turn.
|
|
237
|
+
|
|
238
|
+
The remainder is what the series are given, and it is computed at the reducing precision so
|
|
239
|
+
that the subtraction -- which cancels almost everything when the angle is near a quarter
|
|
240
|
+
turn -- still leaves fifty good digits behind.
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
if abs(angle) > _LARGEST_REDUCIBLE_ANGLE:
|
|
244
|
+
raise ValueError(
|
|
245
|
+
f"an angle of {angle} radians is larger than the {_LARGEST_REDUCIBLE_ANGLE} this "
|
|
246
|
+
f"module reduces exactly; every angle on this Reader's path is bounded by half a "
|
|
247
|
+
f"turn and this one is not one of them"
|
|
248
|
+
)
|
|
249
|
+
with decimal.localcontext(_REDUCING):
|
|
250
|
+
turns = (angle / _HALF_PI).to_integral_value(rounding=decimal.ROUND_HALF_EVEN)
|
|
251
|
+
remainder = angle - turns * _HALF_PI
|
|
252
|
+
return int(turns) % 4, +remainder
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _sine_series(remainder: Decimal) -> Decimal:
|
|
256
|
+
"""``sin`` of an angle already reduced to an eighth of a turn or less."""
|
|
257
|
+
|
|
258
|
+
square = remainder * remainder
|
|
259
|
+
term = remainder
|
|
260
|
+
total = remainder
|
|
261
|
+
index = 1
|
|
262
|
+
while True:
|
|
263
|
+
term = -term * square / ((2 * index) * (2 * index + 1))
|
|
264
|
+
if total + term == total:
|
|
265
|
+
return total
|
|
266
|
+
total += term
|
|
267
|
+
index += 1
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _cosine_series(remainder: Decimal) -> Decimal:
|
|
271
|
+
"""``cos`` of an angle already reduced to an eighth of a turn or less."""
|
|
272
|
+
|
|
273
|
+
square = remainder * remainder
|
|
274
|
+
term = Decimal(1)
|
|
275
|
+
total = Decimal(1)
|
|
276
|
+
index = 1
|
|
277
|
+
while True:
|
|
278
|
+
term = -term * square / ((2 * index - 1) * (2 * index))
|
|
279
|
+
if total + term == total:
|
|
280
|
+
return total
|
|
281
|
+
total += term
|
|
282
|
+
index += 1
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _sine(angle: Decimal) -> Decimal:
|
|
286
|
+
quarter, remainder = _quarter_turns(angle)
|
|
287
|
+
if quarter == 0:
|
|
288
|
+
return _sine_series(remainder)
|
|
289
|
+
if quarter == 1:
|
|
290
|
+
return _cosine_series(remainder)
|
|
291
|
+
if quarter == 2:
|
|
292
|
+
return -_sine_series(remainder)
|
|
293
|
+
return -_cosine_series(remainder)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _cosine(angle: Decimal) -> Decimal:
|
|
297
|
+
quarter, remainder = _quarter_turns(angle)
|
|
298
|
+
if quarter == 0:
|
|
299
|
+
return _cosine_series(remainder)
|
|
300
|
+
if quarter == 1:
|
|
301
|
+
return -_sine_series(remainder)
|
|
302
|
+
if quarter == 2:
|
|
303
|
+
return -_cosine_series(remainder)
|
|
304
|
+
return _sine_series(remainder)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def sin(x: float) -> float:
|
|
308
|
+
"""The sine of an angle in radians."""
|
|
309
|
+
|
|
310
|
+
with decimal.localcontext(_WORKING):
|
|
311
|
+
return float(_sine(_argument(x, "sin")))
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def cos(x: float) -> float:
|
|
315
|
+
"""The cosine of an angle in radians."""
|
|
316
|
+
|
|
317
|
+
with decimal.localcontext(_WORKING):
|
|
318
|
+
return float(_cosine(_argument(x, "cos")))
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def tan(x: float) -> float:
|
|
322
|
+
"""The tangent of an angle in radians, as the quotient of the two computed above.
|
|
323
|
+
|
|
324
|
+
The quarter-turn reduction leaves the cosine of a remainder no larger than an eighth of a
|
|
325
|
+
turn, which is never nearer to zero than about 0.707, so the divisor here is zero only if the
|
|
326
|
+
reduction itself changed. The guard is for that day rather than for any argument.
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
with decimal.localcontext(_WORKING):
|
|
330
|
+
angle = _argument(x, "tan")
|
|
331
|
+
divisor = _cosine(angle)
|
|
332
|
+
if divisor == 0:
|
|
333
|
+
raise ValueError(f"tan has no value at {x!r}, where the cosine is exactly zero")
|
|
334
|
+
return float(_sine(angle) / divisor)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# --- Logarithm, exponential and a fractional power -------------------------------------------
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _logarithm(number: Decimal) -> Decimal:
|
|
341
|
+
"""The natural logarithm, reduced to a mantissa the series converges quickly on.
|
|
342
|
+
|
|
343
|
+
A decimal already carries its own power of ten, so the first reduction is free: shift it out
|
|
344
|
+
and the mantissa is between one and ten. Halving at most three more times brings it between
|
|
345
|
+
the square roots of a half and of two. Each halving is exact -- a mantissa that came from a
|
|
346
|
+
float64 has seventeen digits and there are fifty to hold it -- so nothing is lost getting
|
|
347
|
+
there.
|
|
348
|
+
"""
|
|
349
|
+
|
|
350
|
+
exponent = number.adjusted()
|
|
351
|
+
mantissa = number.scaleb(Decimal(-exponent))
|
|
352
|
+
halvings = 0
|
|
353
|
+
while mantissa >= _ROOT_TWO:
|
|
354
|
+
mantissa = mantissa / 2
|
|
355
|
+
halvings += 1
|
|
356
|
+
return exponent * _LOG_TEN + halvings * _LOG_TWO + _logarithm_series(mantissa)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _exponential(power_of_e: Decimal) -> Decimal:
|
|
360
|
+
"""``e`` to a power, reduced modulo the logarithm of ten so the scaling back is exact.
|
|
361
|
+
|
|
362
|
+
Reducing by ten rather than by two is deliberate: a decimal scales by powers of ten for
|
|
363
|
+
nothing, so the last step of this function moves an exponent rather than performing a
|
|
364
|
+
multiplication that would have to be rounded.
|
|
365
|
+
"""
|
|
366
|
+
|
|
367
|
+
with decimal.localcontext(_REDUCING):
|
|
368
|
+
scale = int((power_of_e / _LOG_TEN).to_integral_value(rounding=decimal.ROUND_HALF_EVEN))
|
|
369
|
+
remainder = +(power_of_e - scale * _LOG_TEN)
|
|
370
|
+
term = Decimal(1)
|
|
371
|
+
total = Decimal(1)
|
|
372
|
+
index = 1
|
|
373
|
+
while True:
|
|
374
|
+
term = term * remainder / index
|
|
375
|
+
if total + term == total:
|
|
376
|
+
return total.scaleb(Decimal(scale))
|
|
377
|
+
total += term
|
|
378
|
+
index += 1
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def log(x: float) -> float:
|
|
382
|
+
"""The natural logarithm of a positive number."""
|
|
383
|
+
|
|
384
|
+
with decimal.localcontext(_WORKING):
|
|
385
|
+
number = _argument(x, "log")
|
|
386
|
+
if number <= 0:
|
|
387
|
+
raise ValueError(f"log takes a positive number, not {x!r}")
|
|
388
|
+
return float(_logarithm(number))
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def power(base: float, exponent: float) -> float:
|
|
392
|
+
"""A positive base raised to any power, as ``exp(exponent * log(base))``.
|
|
393
|
+
|
|
394
|
+
Written as one function rather than left to ``**`` because the operator on two floats is the
|
|
395
|
+
C library's ``pow``, which is exactly the kind of call this module exists to remove. The
|
|
396
|
+
base is required to be positive: every use of it here raises a tangent or a radius, both of
|
|
397
|
+
which are checked positive by their caller before they arrive.
|
|
398
|
+
"""
|
|
399
|
+
|
|
400
|
+
with decimal.localcontext(_WORKING):
|
|
401
|
+
bottom = _argument(base, "power")
|
|
402
|
+
top = _argument(exponent, "power")
|
|
403
|
+
if bottom <= 0:
|
|
404
|
+
raise ValueError(f"power takes a positive base, not {base!r}")
|
|
405
|
+
if top == 0 or bottom == 1:
|
|
406
|
+
return 1.0
|
|
407
|
+
return float(_exponential(_logarithm(bottom) * top))
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
# --- Arc tangent, in one and in two arguments ------------------------------------------------
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _arc_tangent_series(x: Decimal) -> Decimal:
|
|
414
|
+
"""``atan`` of an argument the caller has already halved down below a twentieth."""
|
|
415
|
+
|
|
416
|
+
square = x * x
|
|
417
|
+
signed = x
|
|
418
|
+
total = x
|
|
419
|
+
index = 1
|
|
420
|
+
while True:
|
|
421
|
+
signed = -signed * square
|
|
422
|
+
addend = signed / (2 * index + 1)
|
|
423
|
+
if total + addend == total:
|
|
424
|
+
return total
|
|
425
|
+
total += addend
|
|
426
|
+
index += 1
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _arc_tangent(x: Decimal) -> Decimal:
|
|
430
|
+
"""The arc tangent, by reciprocal above one and by repeated halving below it.
|
|
431
|
+
|
|
432
|
+
``atan(x) = 2 atan(x / (1 + sqrt(1 + x^2)))`` is the halving identity, and four or five of
|
|
433
|
+
them carry any argument in the unit interval below the series threshold.
|
|
434
|
+
"""
|
|
435
|
+
|
|
436
|
+
if x < 0:
|
|
437
|
+
return -_arc_tangent(-x)
|
|
438
|
+
if x > 1:
|
|
439
|
+
return _HALF_PI - _arc_tangent(1 / x)
|
|
440
|
+
halvings = 0
|
|
441
|
+
while x > _ARC_TANGENT_THRESHOLD:
|
|
442
|
+
x = x / (1 + (1 + x * x).sqrt())
|
|
443
|
+
halvings += 1
|
|
444
|
+
return (2**halvings) * _arc_tangent_series(x)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def atan(x: float) -> float:
|
|
448
|
+
"""The arc tangent of a number, in radians, between minus and plus a quarter turn."""
|
|
449
|
+
|
|
450
|
+
with decimal.localcontext(_WORKING):
|
|
451
|
+
return float(_arc_tangent(_argument(x, "atan")))
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def atan2(y: float, x: float) -> float:
|
|
455
|
+
"""The angle of the point ``(x, y)`` from the positive x axis, in radians.
|
|
456
|
+
|
|
457
|
+
Signed zeros are not distinguished: a zero is a zero here, so ``atan2(0.0, -1.0)`` is half a
|
|
458
|
+
turn and never minus half a turn. The one caller in this Reader guards against both
|
|
459
|
+
arguments being zero before it calls, because a point at the apex of the cone has every
|
|
460
|
+
longitude and is refused there by name rather than answered here by convention.
|
|
461
|
+
"""
|
|
462
|
+
|
|
463
|
+
with decimal.localcontext(_WORKING):
|
|
464
|
+
across = _argument(x, "atan2")
|
|
465
|
+
up = _argument(y, "atan2")
|
|
466
|
+
if across == 0:
|
|
467
|
+
if up == 0:
|
|
468
|
+
return 0.0
|
|
469
|
+
return float(_HALF_PI if up > 0 else -_HALF_PI)
|
|
470
|
+
angle = _arc_tangent(up / across)
|
|
471
|
+
if across > 0:
|
|
472
|
+
return float(angle)
|
|
473
|
+
return float(angle + _PI if up >= 0 else angle - _PI)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
# --- The rest: exact float64 work, kept here so the projection has one place to look ---------
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def hypot(x: float, y: float) -> float:
|
|
480
|
+
"""The length of the hypotenuse, from a square root the specification rounds correctly."""
|
|
481
|
+
|
|
482
|
+
with decimal.localcontext(_WORKING):
|
|
483
|
+
across = _argument(x, "hypot")
|
|
484
|
+
up = _argument(y, "hypot")
|
|
485
|
+
return float((across * across + up * up).sqrt())
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def radians(degrees_of_arc: float) -> float:
|
|
489
|
+
"""Degrees to radians: one multiplication, correctly rounded on every machine."""
|
|
490
|
+
|
|
491
|
+
if isinstance(degrees_of_arc, bool) or not isinstance(degrees_of_arc, (int, float)):
|
|
492
|
+
raise TypeError(f"radians takes a real number, not {degrees_of_arc!r}")
|
|
493
|
+
return degrees_of_arc * _DEGREES_TO_RADIANS
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def degrees(radians_of_arc: float) -> float:
|
|
497
|
+
"""Radians to degrees: one multiplication, correctly rounded on every machine."""
|
|
498
|
+
|
|
499
|
+
if isinstance(radians_of_arc, bool) or not isinstance(radians_of_arc, (int, float)):
|
|
500
|
+
raise TypeError(f"degrees takes a real number, not {radians_of_arc!r}")
|
|
501
|
+
return radians_of_arc * _RADIANS_TO_DEGREES
|