openscript 0.6.0__tar.gz → 0.7.0__tar.gz
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.
- {openscript-0.6.0 → openscript-0.7.0}/PKG-INFO +11 -1
- {openscript-0.6.0 → openscript-0.7.0}/README.md +10 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/library/__init__.py +5 -7
- {openscript-0.6.0 → openscript-0.7.0}/openscript/library/arithmetic.py +5 -4
- {openscript-0.6.0 → openscript-0.7.0}/openscript/library/averages.py +7 -14
- {openscript-0.6.0 → openscript-0.7.0}/openscript/library/bookkeeping.py +8 -12
- {openscript-0.6.0 → openscript-0.7.0}/openscript/library/composites.py +10 -18
- {openscript-0.6.0 → openscript-0.7.0}/openscript/library/deviation.py +4 -3
- openscript-0.7.0/openscript/library/elementary.py +146 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/library/flows.py +10 -8
- openscript-0.7.0/openscript/library/gaussian.py +51 -0
- openscript-0.7.0/openscript/library/history.py +72 -0
- openscript-0.7.0/openscript/library/hypotenuse.py +65 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/library/momentum.py +8 -13
- openscript-0.7.0/openscript/library/power.py +79 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/library/ranges.py +2 -4
- {openscript-0.6.0 → openscript-0.7.0}/openscript/library/series.py +45 -30
- {openscript-0.6.0 → openscript-0.7.0}/openscript/library/strength.py +16 -6
- openscript-0.7.0/openscript/library/transcendental.py +166 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/library/trend.py +26 -25
- openscript-0.7.0/openscript/library/trigonometric.py +200 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/memory.py +10 -2
- {openscript-0.6.0 → openscript-0.7.0}/openscript.egg-info/PKG-INFO +11 -1
- {openscript-0.6.0 → openscript-0.7.0}/openscript.egg-info/SOURCES.txt +24 -1
- {openscript-0.6.0 → openscript-0.7.0}/pyproject.toml +1 -1
- openscript-0.7.0/tests/test_hypot.py +156 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_library_vectors.py +7 -68
- openscript-0.7.0/tests/test_mfi_numeric_edges.py +146 -0
- openscript-0.7.0/tests/test_momentum_overflow.py +186 -0
- openscript-0.7.0/tests/test_power.py +136 -0
- openscript-0.7.0/tests/test_range_numeric_edges.py +47 -0
- openscript-0.7.0/tests/test_recursive_overflow.py +83 -0
- openscript-0.7.0/tests/test_stateful_numeric_edges.py +82 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_stateful_vectors.py +7 -74
- openscript-0.7.0/tests/test_statistics_overflow.py +112 -0
- openscript-0.7.0/tests/test_transcendental.py +174 -0
- openscript-0.7.0/tests/test_trend_numeric_edges.py +116 -0
- openscript-0.7.0/tests/test_trigonometric.py +135 -0
- openscript-0.7.0/tests/test_varying_distance.py +60 -0
- openscript-0.7.0/tests/test_varying_lookback.py +158 -0
- openscript-0.7.0/tests/test_varying_occurrence.py +67 -0
- openscript-0.7.0/tests/test_varying_recursive.py +120 -0
- openscript-0.7.0/tests/test_volume_numeric_edges.py +121 -0
- openscript-0.7.0/tests/test_zero_boundaries.py +59 -0
- openscript-0.6.0/openscript/library/elementary.py +0 -206
- {openscript-0.6.0 → openscript-0.7.0}/openscript/__init__.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/__main__.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/accounting/__init__.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/accounting/analysis.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/accounting/charges.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/accounting/equity.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/accounting/report.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/accounting/shapes.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/accounting/statistics.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/accounting/trades.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/__init__.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/answers.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/channels.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/expectations.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/facts.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/matching.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/ordering.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/page.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/reading.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/reporting.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/running.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/secondary.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/serving.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/sessions.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/spellings.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/surface.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/arrays.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/bars.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/buckets.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/budget.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/canonical.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/civil.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/contracts.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/dates.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/diagnostics.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/hours.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/inputs.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/intervals.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/library/bars.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/library/code_points.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/library/colour.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/library/counting.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/library/extremes.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/library/number_text.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/library/prices.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/library/rounding.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/library/stateful.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/library/stateless.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/library/strings.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/library/values.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/logbook.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/machine.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/objects.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/opcodes.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/program.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/reasons.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/request_body.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/request_plan.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/requests.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/run.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/strategy/__init__.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/strategy/calls.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/strategy/closable.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/strategy/fills.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/strategy/holdings.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/strategy/intents.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/strategy/ledger.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/strategy/placing.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/strategy/positions.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/strategy/refusals.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/strategy/rows.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/strategy/sizing.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/strategy/statuses.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/surface/__init__.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/surface/bands.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/surface/levels.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/surface/marks.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/surface/paints.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/surface/plots.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/surface/published.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/values.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/verify.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/verify_code.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/verify_requests.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/verify_shape.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/verify_tables.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/version.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript/zones.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript.egg-info/dependency_links.txt +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/openscript.egg-info/top_level.txt +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/setup.cfg +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_adapter_cases.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_adapter_identity.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_adapter_matching.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_adapter_page.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_adapter_spellings.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_analysis_and_runup.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_arrays.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_bar_cycle.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_bars.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_calendar.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_calendar_reads.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_calls.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_canonical.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_charges.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_colour.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_cost_models.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_diagnostics.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_engine_facts.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_frame_fold.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_frame_instant.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_hand_over.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_harness.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_host_surface.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_ledger_orders.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_logbook.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_machine.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_maths_rules.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_memory.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_number_text.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_objects.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_opcodes.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_order_page.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_order_refusals.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_programs.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_request_host.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_requests.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_series_rules.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_session_window.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_sizing_refusal.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_strategy_cases.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_strategy_profile.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_string_ceiling.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_strings.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_supplied_schedule.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_surface.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_surface_bands.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_surface_example.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_time_programs.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_trades_and_summary.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_values.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_verify.py +0 -0
- {openscript-0.6.0 → openscript-0.7.0}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openscript
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: An open trading language: the engine that runs a compiled program.
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Homepage, https://github.com/marketcalls/openscript#readme
|
|
@@ -39,6 +39,16 @@ where any disagreement is a release blocker.
|
|
|
39
39
|
stop a run being reproducible. Measured on every build against the module
|
|
40
40
|
names the running interpreter says are its own.
|
|
41
41
|
|
|
42
|
+
## Numerical results in 0.7.0
|
|
43
|
+
|
|
44
|
+
The two engines use the same specified rounding, seeds and missing-value rules.
|
|
45
|
+
Portable elementary kernels remove host-dependent final-bit differences; the
|
|
46
|
+
release audit requires exact agreement for every numerical signature it covers,
|
|
47
|
+
including forming-bar replacement and checkpoint replay. Install the matching
|
|
48
|
+
0.7.0 compiler package when comparing browser and server results. Source calls
|
|
49
|
+
and the compiled format are unchanged, but corrected results can differ from
|
|
50
|
+
previous releases.
|
|
51
|
+
|
|
42
52
|
## Installing
|
|
43
53
|
|
|
44
54
|
```
|
|
@@ -25,6 +25,16 @@ where any disagreement is a release blocker.
|
|
|
25
25
|
stop a run being reproducible. Measured on every build against the module
|
|
26
26
|
names the running interpreter says are its own.
|
|
27
27
|
|
|
28
|
+
## Numerical results in 0.7.0
|
|
29
|
+
|
|
30
|
+
The two engines use the same specified rounding, seeds and missing-value rules.
|
|
31
|
+
Portable elementary kernels remove host-dependent final-bit differences; the
|
|
32
|
+
release audit requires exact agreement for every numerical signature it covers,
|
|
33
|
+
including forming-bar replacement and checkpoint replay. Install the matching
|
|
34
|
+
0.7.0 compiler package when comparing browser and server results. Source calls
|
|
35
|
+
and the compiled format are unchanged, but corrected results can differ from
|
|
36
|
+
previous releases.
|
|
37
|
+
|
|
28
38
|
## Installing
|
|
29
39
|
|
|
30
40
|
```
|
|
@@ -13,7 +13,7 @@ What is here, by the page it is written from:
|
|
|
13
13
|
- ``values`` absence, and the two rules every number returned has passed
|
|
14
14
|
- ``arithmetic`` section 8.1, the bare calls that compute one value
|
|
15
15
|
- ``rounding`` section 8.1 and 20.7, one rule for halves and one scale table
|
|
16
|
-
- ``elementary`` section 8.2
|
|
16
|
+
- ``elementary`` section 8.2 and the portable arithmetic of section 20.10
|
|
17
17
|
- ``bars`` section 20.5's one reading that remembers nothing
|
|
18
18
|
- ``code_points`` section 10's whitespace set and the one string order
|
|
19
19
|
- ``number_text`` `language.md` 5.5, how a number becomes text and back
|
|
@@ -50,12 +50,10 @@ mechanical copy without the engine knowing which function it belongs to. A
|
|
|
50
50
|
stateless call needs none, which is what lets an engine call one of those from
|
|
51
51
|
anywhere without a checkpoint or a rollback.
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
anywhere, so they call the host's maths module and carry no cross-engine
|
|
58
|
-
guarantee. ``elementary`` says it again where an implementer will be standing.
|
|
53
|
+
Power and the trigonometric namespace still reach gap 1 of `stdlib.md` section
|
|
54
|
+
20.11. Hypotenuse follows section 20.10.1, while exponential and logarithmic
|
|
55
|
+
functions follow section 20.10.2. Those portable kernels and every stateful
|
|
56
|
+
reading, including ``alma``, ``hv`` and ``chop``, are held to exact vector bits.
|
|
59
57
|
"""
|
|
60
58
|
|
|
61
59
|
from .stateful import ENTRIES as STATEFUL_ENTRIES
|
|
@@ -12,7 +12,7 @@ exception and its order is written out in section 8.1 and confirmed in 20.7: the
|
|
|
12
12
|
division, then the floor, then the multiplication, then the subtraction.
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
from .values import ABSENT, Value, floor_of, number, result
|
|
15
|
+
from .values import ABSENT, Value, floor_of, is_number, number, result
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def abs_of(x: Value) -> Value:
|
|
@@ -110,10 +110,11 @@ def is_none(x: Value) -> bool:
|
|
|
110
110
|
def or_else(x: Value, fallback: Value) -> Value:
|
|
111
111
|
"""``orElse(x, fallback)``: ``x`` when present, ``fallback`` when absent.
|
|
112
112
|
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
Absence and every nonnumeric tag keep their identity; numeric zero follows
|
|
114
|
+
the same positive-zero rule as other library results.
|
|
115
115
|
"""
|
|
116
|
-
|
|
116
|
+
value = fallback if x is None else x
|
|
117
|
+
return 0.0 if is_number(value) and value == 0 else value
|
|
117
118
|
|
|
118
119
|
|
|
119
120
|
def to_bool(x: Value) -> bool:
|
|
@@ -6,10 +6,9 @@ rather than one is the step and the weighting, and what makes each of them one
|
|
|
6
6
|
function rather than a family is the arrangement, which is why every entry here
|
|
7
7
|
names the arrangement it is not.
|
|
8
8
|
|
|
9
|
-
A value arrives already read off this bar's arguments
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
bars it is supposed to cover.
|
|
9
|
+
A value arrives already read off this bar's arguments. Every invocation counts
|
|
10
|
+
as a contribution, including one with an absent length. Finite windows retain
|
|
11
|
+
their contributions; a seeded recurrence releases its history after seeding.
|
|
13
12
|
"""
|
|
14
13
|
|
|
15
14
|
from .series import Region, contributed, mean, seeded, total, window
|
|
@@ -34,16 +33,13 @@ def exponential(state: Region, value: Value, length: int | None) -> Value:
|
|
|
34
33
|
rather than two, and over the fixture the release gate compares bit for bit
|
|
35
34
|
it differs on most of the values at every length.
|
|
36
35
|
"""
|
|
37
|
-
|
|
38
|
-
if length is None:
|
|
39
|
-
return ABSENT
|
|
40
|
-
weight = 2 / (length + 1)
|
|
36
|
+
weight = 0.0 if length is None else 2 / (length + 1)
|
|
41
37
|
rest = 1 - weight
|
|
42
38
|
|
|
43
39
|
def step(running: float, value: float) -> float:
|
|
44
40
|
return value * weight + running * rest
|
|
45
41
|
|
|
46
|
-
return seeded(state, "run",
|
|
42
|
+
return seeded(state, "run", number(value), length, step)
|
|
47
43
|
|
|
48
44
|
|
|
49
45
|
def smoothed(state: Region, value: Value, length: int | None) -> Value:
|
|
@@ -56,15 +52,12 @@ def smoothed(state: Region, value: Value, length: int | None) -> Value:
|
|
|
56
52
|
most often asked to reproduce. Neither is
|
|
57
53
|
``running + (value - running) / len``, which is a third arrangement again.
|
|
58
54
|
"""
|
|
59
|
-
|
|
60
|
-
if length is None:
|
|
61
|
-
return ABSENT
|
|
62
|
-
span = float(length)
|
|
55
|
+
span = 1.0 if length is None else float(length)
|
|
63
56
|
|
|
64
57
|
def step(running: float, value: float) -> float:
|
|
65
58
|
return (running * (span - 1) + value) / span
|
|
66
59
|
|
|
67
|
-
return seeded(state, "run",
|
|
60
|
+
return seeded(state, "run", number(value), length, step)
|
|
68
61
|
|
|
69
62
|
|
|
70
63
|
def linear(state: Region, value: Value, length: int | None) -> Value:
|
|
@@ -21,6 +21,7 @@ make the same way twice for two engines to agree:
|
|
|
21
21
|
|
|
22
22
|
from typing import Optional
|
|
23
23
|
|
|
24
|
+
from .history import ContributionHistory
|
|
24
25
|
from .series import Region, back, contributed, raw_window, region
|
|
25
26
|
from .values import ABSENT, Value, number, result
|
|
26
27
|
|
|
@@ -150,17 +151,12 @@ def value_when(
|
|
|
150
151
|
"""
|
|
151
152
|
wanted = 0 if occurrence is None else occurrence
|
|
152
153
|
held = region(state, "seen")
|
|
153
|
-
|
|
154
|
-
if
|
|
155
|
-
|
|
156
|
-
held["values"] = values
|
|
154
|
+
history = held.get("history")
|
|
155
|
+
if not isinstance(history, ContributionHistory):
|
|
156
|
+
history = ContributionHistory()
|
|
157
157
|
if condition is True:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
extra = len(values) - depth
|
|
162
|
-
if extra > 0:
|
|
163
|
-
del values[:extra]
|
|
164
|
-
if wanted < 0 or len(values) <= wanted:
|
|
158
|
+
history = history.append(value, None)
|
|
159
|
+
held["history"] = history
|
|
160
|
+
if wanted < 0 or history.count <= wanted:
|
|
165
161
|
return ABSENT
|
|
166
|
-
return
|
|
162
|
+
return history.view(wanted + 1)[0]
|
|
@@ -6,8 +6,8 @@ inner one produced, not on the first bars of the source. That composition is the
|
|
|
6
6
|
whole reason `stdlib.md` section 1 states warmups in bars of the call's own
|
|
7
7
|
source rather than in bars of the chart.
|
|
8
8
|
|
|
9
|
-
The other two compute over the window directly: the Gaussian kernel
|
|
10
|
-
|
|
9
|
+
The other two compute over the window directly: the portable Gaussian kernel
|
|
10
|
+
of section 20.10.2, and the least
|
|
11
11
|
squares fit, whose constants are functions of the length alone.
|
|
12
12
|
|
|
13
13
|
``named`` is here rather than beside the six means because it has to be able to
|
|
@@ -17,6 +17,7 @@ select any of them, this file's included.
|
|
|
17
17
|
import math
|
|
18
18
|
|
|
19
19
|
from . import averages, elementary
|
|
20
|
+
from .gaussian import gaussian_weights
|
|
20
21
|
from .rounding import round_half_away
|
|
21
22
|
from .series import Region, contributed, region, window
|
|
22
23
|
from .values import ABSENT, Value, number, result, whole
|
|
@@ -140,10 +141,8 @@ def gaussian(
|
|
|
140
141
|
order. The denominator of the exponent is **one product, divided once**: the
|
|
141
142
|
chain of divisions in circulation differs on about one exponent in four.
|
|
142
143
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
only on the position, so an engine may build it once per length, provided the
|
|
146
|
-
values it builds are the ones these lines produce.
|
|
144
|
+
A bounded pure cache holds immutable weights for exact parameter values.
|
|
145
|
+
It holds no source observations and does not enter checkpoint state.
|
|
147
146
|
"""
|
|
148
147
|
values = contributed(state, "src", number(value), length)
|
|
149
148
|
held = window(values, length)
|
|
@@ -151,19 +150,12 @@ def gaussian(
|
|
|
151
150
|
width = number(sigma)
|
|
152
151
|
if held is None or length is None or peak_at is None or width is None:
|
|
153
152
|
return ABSENT
|
|
154
|
-
if width
|
|
153
|
+
if width <= 0:
|
|
155
154
|
return ABSENT
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
norm =
|
|
160
|
-
for position in range(length):
|
|
161
|
-
gap = position - peak
|
|
162
|
-
weight = elementary.exp(-(gap * gap) / (2 * spread * spread))
|
|
163
|
-
if not isinstance(weight, float):
|
|
164
|
-
return ABSENT
|
|
165
|
-
weights.append(weight)
|
|
166
|
-
norm = norm + weight
|
|
155
|
+
kernel = gaussian_weights(length, peak_at, width)
|
|
156
|
+
if kernel is None:
|
|
157
|
+
return ABSENT
|
|
158
|
+
weights, norm = kernel
|
|
167
159
|
running = 0.0
|
|
168
160
|
for position in range(length):
|
|
169
161
|
running = running + held[length - 1 - position] * weights[position]
|
|
@@ -128,18 +128,19 @@ def historical(
|
|
|
128
128
|
of log returns and says nothing about a percentage, so a study that wants a
|
|
129
129
|
percentage axis multiplies at the plot, where a reader can see it happen.
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
carries no cross-engine guarantee.
|
|
131
|
+
The logarithm follows the portable interval recipe of section 20.10.2.
|
|
133
132
|
"""
|
|
134
133
|
values = contributed(state, "src", number(value), 2)
|
|
135
134
|
now = values[len(values) - 1]
|
|
136
135
|
before = values[len(values) - 2] if len(values) > 1 else ABSENT
|
|
137
136
|
ratio = ABSENT
|
|
138
|
-
if isinstance(now, float) and isinstance(before, float) and before
|
|
137
|
+
if isinstance(now, float) and isinstance(before, float) and now > 0 and before > 0:
|
|
139
138
|
ratio = now / before
|
|
140
139
|
step = elementary.log(ratio)
|
|
141
140
|
spread = deviation(region(state, "spread"), step, length, False)
|
|
142
141
|
count = number(periods)
|
|
142
|
+
if count is None or count <= 0:
|
|
143
|
+
return ABSENT
|
|
143
144
|
scale = elementary.sqrt(count)
|
|
144
145
|
if not isinstance(spread, float) or not isinstance(scale, float):
|
|
145
146
|
return ABSENT
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Elementary numeric functions with reproducible rounding.
|
|
2
|
+
|
|
3
|
+
Hypotenuse and exp/log use the exact integer recipes in sections 20.10.1 and
|
|
4
|
+
20.10.2; power and trigonometry use sections 20.10.3 and 20.10.4.
|
|
5
|
+
Square root uses its correctly rounded host operation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
|
|
10
|
+
from .hypotenuse import hypotenuse
|
|
11
|
+
from .transcendental import transcendental
|
|
12
|
+
from .power import power as real_power
|
|
13
|
+
from .trigonometric import trigonometric
|
|
14
|
+
from .values import ABSENT, Value, number, result
|
|
15
|
+
|
|
16
|
+
# The circle constant and the base of the natural logarithm, as `stdlib.md`
|
|
17
|
+
# section 8.2 names them. Both are the nearest binary64 to the constant and are
|
|
18
|
+
# the same bits on every platform.
|
|
19
|
+
PI: float = math.pi
|
|
20
|
+
E: float = math.e
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def sqrt(x: Value) -> Value:
|
|
24
|
+
"""``sqrt(x)``: square root, absent below zero."""
|
|
25
|
+
value = number(x)
|
|
26
|
+
if value is None or value < 0:
|
|
27
|
+
return ABSENT
|
|
28
|
+
return result(math.sqrt(value))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def exp(x: Value) -> Value:
|
|
32
|
+
"""``exp(x)``: e to the power x, using section 20.10.2."""
|
|
33
|
+
value = number(x)
|
|
34
|
+
return ABSENT if value is None else transcendental('exp', value)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def log(x: Value) -> Value:
|
|
38
|
+
"""``log(x)``: portable natural logarithm, absent at or below zero."""
|
|
39
|
+
value = number(x)
|
|
40
|
+
if value is None or value <= 0:
|
|
41
|
+
return ABSENT
|
|
42
|
+
return transcendental('log', value)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def log10(x: Value) -> Value:
|
|
46
|
+
"""``log10(x)``: portable base ten logarithm, same absence rule."""
|
|
47
|
+
value = number(x)
|
|
48
|
+
if value is None or value <= 0:
|
|
49
|
+
return ABSENT
|
|
50
|
+
return transcendental('log10', value)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def log2(x: Value) -> Value:
|
|
54
|
+
"""``math.log2(x)``: portable base two logarithm, same absence rule."""
|
|
55
|
+
value = number(x)
|
|
56
|
+
if value is None or value <= 0:
|
|
57
|
+
return ABSENT
|
|
58
|
+
return transcendental('log2', value)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def power(x: Value, y: Value) -> Value:
|
|
62
|
+
"""``pow(x, y)``: portable real power, using section 20.10.3."""
|
|
63
|
+
base = number(x)
|
|
64
|
+
exponent = number(y)
|
|
65
|
+
if base is None or exponent is None:
|
|
66
|
+
return ABSENT
|
|
67
|
+
return real_power(base, exponent)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def hypot(x: Value, y: Value) -> Value:
|
|
71
|
+
"""``math.hypot(x, y)``: the correctly rounded exact diagonal, section 20.10.1."""
|
|
72
|
+
left = number(x)
|
|
73
|
+
right = number(y)
|
|
74
|
+
if left is None or right is None:
|
|
75
|
+
return ABSENT
|
|
76
|
+
return hypotenuse(left, right)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def to_degrees(x: Value) -> Value:
|
|
80
|
+
"""``math.toDegrees(x)``: ``(x * 180) / pi``, multiply first.
|
|
81
|
+
|
|
82
|
+
Not a gap: it is two operations over binary64 and `stdlib.md` section 20.7
|
|
83
|
+
fixes their order. Folding the constant into one factor is a different number
|
|
84
|
+
at about a quarter of the arguments, so the association is written out rather
|
|
85
|
+
than handed to a conversion the host provides.
|
|
86
|
+
"""
|
|
87
|
+
value = number(x)
|
|
88
|
+
return ABSENT if value is None else result((value * 180) / math.pi)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def to_radians(x: Value) -> Value:
|
|
92
|
+
"""``math.toRadians(x)``: ``(x * pi) / 180``, multiply first. Not a gap."""
|
|
93
|
+
value = number(x)
|
|
94
|
+
return ABSENT if value is None else result((value * math.pi) / 180)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def sin(x: Value) -> Value:
|
|
98
|
+
"""``math.sin(x)``: sine of an angle in radians, using section 20.10.4."""
|
|
99
|
+
value = number(x)
|
|
100
|
+
return ABSENT if value is None else trigonometric('sin', value)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def cos(x: Value) -> Value:
|
|
104
|
+
"""``math.cos(x)``: cosine, using section 20.10.4."""
|
|
105
|
+
value = number(x)
|
|
106
|
+
return ABSENT if value is None else trigonometric('cos', value)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def tan(x: Value) -> Value:
|
|
110
|
+
"""``math.tan(x)``: tangent, using section 20.10.4."""
|
|
111
|
+
value = number(x)
|
|
112
|
+
return ABSENT if value is None else trigonometric('tan', value)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def asin(x: Value) -> Value:
|
|
116
|
+
"""``math.asin(x)``: inverse sine, absent outside -1 to 1."""
|
|
117
|
+
value = number(x)
|
|
118
|
+
if value is None or value < -1 or value > 1:
|
|
119
|
+
return ABSENT
|
|
120
|
+
return trigonometric('asin', value)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def acos(x: Value) -> Value:
|
|
124
|
+
"""``math.acos(x)``: inverse cosine, same range rule."""
|
|
125
|
+
value = number(x)
|
|
126
|
+
if value is None or value < -1 or value > 1:
|
|
127
|
+
return ABSENT
|
|
128
|
+
return trigonometric('acos', value)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def atan(x: Value) -> Value:
|
|
132
|
+
"""``math.atan(x)``: inverse tangent, using section 20.10.4."""
|
|
133
|
+
value = number(x)
|
|
134
|
+
return ABSENT if value is None else trigonometric('atan', value)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def atan2(y: Value, x: Value) -> Value:
|
|
138
|
+
"""``math.atan2(y, x)``: angle of a vector, in all four quadrants.
|
|
139
|
+
|
|
140
|
+
The vertical component comes first, as section 20.10.4 specifies.
|
|
141
|
+
"""
|
|
142
|
+
vertical = number(y)
|
|
143
|
+
horizontal = number(x)
|
|
144
|
+
if vertical is None or horizontal is None:
|
|
145
|
+
return ABSENT
|
|
146
|
+
return trigonometric('atan2', vertical, horizontal)
|
|
@@ -44,11 +44,12 @@ def _anchored(state: Region, ctx, value: Value, anchor: Value) -> Value:
|
|
|
44
44
|
held = region(state, "vwap")
|
|
45
45
|
price = number(value)
|
|
46
46
|
traded = number(ctx.bar("volume"))
|
|
47
|
-
if anchor is
|
|
48
|
-
return ABSENT
|
|
49
|
-
if anchor:
|
|
47
|
+
if anchor is True:
|
|
50
48
|
held["flow"] = 0.0
|
|
51
49
|
held["traded"] = 0.0
|
|
50
|
+
held["anchored"] = True
|
|
51
|
+
if held.get("anchored") is not True:
|
|
52
|
+
return ABSENT
|
|
52
53
|
if price is None or traded is None:
|
|
53
54
|
return ABSENT
|
|
54
55
|
flow = held.get("flow", 0.0) + price * traded
|
|
@@ -113,10 +114,11 @@ def flow_fraction(state: Region, ctx, length: Optional[int]) -> Value:
|
|
|
113
114
|
traded = window(volumes, length)
|
|
114
115
|
if held is None or traded is None:
|
|
115
116
|
return ABSENT
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
flow = result(total(held))
|
|
118
|
+
divisor = result(total(traded))
|
|
119
|
+
if flow is None or divisor is None or divisor == 0:
|
|
118
120
|
return ABSENT
|
|
119
|
-
return result(
|
|
121
|
+
return result(flow / divisor)
|
|
120
122
|
|
|
121
123
|
|
|
122
124
|
def accumulation_gap(
|
|
@@ -164,14 +166,14 @@ def money_flow(state: Region, ctx, length: Optional[int]) -> Value:
|
|
|
164
166
|
rise = ABSENT
|
|
165
167
|
fall = ABSENT
|
|
166
168
|
if isinstance(price, float) and traded is not None and isinstance(before, float):
|
|
167
|
-
flow = price * traded
|
|
169
|
+
flow = result(price * traded)
|
|
168
170
|
rise = flow if price > before else 0.0
|
|
169
171
|
fall = flow if price < before else 0.0
|
|
170
172
|
rises = window(contributed(state, "rise", rise, length), length)
|
|
171
173
|
falls = window(contributed(state, "fall", fall, length), length)
|
|
172
174
|
if rises is None or falls is None:
|
|
173
175
|
return ABSENT
|
|
174
|
-
return strength.line_of(total(rises), total(falls))
|
|
176
|
+
return strength.line_of(result(total(rises)), result(total(falls)))
|
|
175
177
|
|
|
176
178
|
|
|
177
179
|
def movement(state: Region, ctx, length: Optional[int]) -> Value:
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Bounded immutable Gaussian coefficients, independent of checkpoint state."""
|
|
2
|
+
import math
|
|
3
|
+
import struct
|
|
4
|
+
|
|
5
|
+
from .elementary import exp
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GaussianCache:
|
|
9
|
+
"""FIFO with eight parameter tuples and at most 4096 coefficients."""
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self.entries = {}
|
|
12
|
+
self.coefficients = 0
|
|
13
|
+
|
|
14
|
+
def get(self, length, offset, sigma):
|
|
15
|
+
if not math.isfinite(offset) or not math.isfinite(sigma) or sigma <= 0:
|
|
16
|
+
return None
|
|
17
|
+
key = (length, struct.pack('>d', offset), struct.pack('>d', sigma))
|
|
18
|
+
if key in self.entries:
|
|
19
|
+
return self.entries[key]
|
|
20
|
+
peak, spread = offset * (length - 1), length / sigma
|
|
21
|
+
denominator = (2 * spread) * spread
|
|
22
|
+
if denominator == 0:
|
|
23
|
+
return None
|
|
24
|
+
weights = []
|
|
25
|
+
norm = 0.0
|
|
26
|
+
for position in range(length):
|
|
27
|
+
gap = position - peak
|
|
28
|
+
exponent = -(gap * gap) / denominator
|
|
29
|
+
weight = 0.0 if exponent == -math.inf else exp(exponent)
|
|
30
|
+
if weight is None:
|
|
31
|
+
return None
|
|
32
|
+
weights.append(weight)
|
|
33
|
+
norm += weight
|
|
34
|
+
if norm == 0:
|
|
35
|
+
return None
|
|
36
|
+
computed = (tuple(weights), norm)
|
|
37
|
+
if length <= 4096:
|
|
38
|
+
while len(self.entries) >= 8 or self.coefficients + length > 4096:
|
|
39
|
+
oldest = next(iter(self.entries))
|
|
40
|
+
self.coefficients -= len(self.entries[oldest][0])
|
|
41
|
+
del self.entries[oldest]
|
|
42
|
+
self.entries[key] = computed
|
|
43
|
+
self.coefficients += length
|
|
44
|
+
return computed
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_CACHE = GaussianCache()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def gaussian_weights(length, offset, sigma):
|
|
51
|
+
return _CACHE.get(length, offset, sigma)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Immutable contributions with bounded append copying and cheap checkpoints."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional, Sequence
|
|
5
|
+
|
|
6
|
+
from .values import Value
|
|
7
|
+
|
|
8
|
+
_WIDTH = 32
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class _Chunk:
|
|
13
|
+
values: tuple[Value, ...]
|
|
14
|
+
prior: Optional["_Chunk"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class ContributionHistory:
|
|
19
|
+
"""A version shares its sealed prefix with previous versions and forks."""
|
|
20
|
+
|
|
21
|
+
head: Optional[_Chunk] = None
|
|
22
|
+
tail: tuple[Value, ...] = ()
|
|
23
|
+
count: int = 0
|
|
24
|
+
maximum: int = 0
|
|
25
|
+
|
|
26
|
+
def append(self, value: Value, length: Optional[int]) -> "ContributionHistory":
|
|
27
|
+
full = len(self.tail) == _WIDTH
|
|
28
|
+
head = _Chunk(self.tail, self.head) if full else self.head
|
|
29
|
+
tail = (value,) if full else self.tail + (value,)
|
|
30
|
+
maximum = max(self.maximum, length or 0)
|
|
31
|
+
return ContributionHistory(head, tail, self.count + 1, maximum)
|
|
32
|
+
|
|
33
|
+
def __deepcopy__(self, memo):
|
|
34
|
+
"""All reachable state is immutable, so checkpoint copying shares it."""
|
|
35
|
+
return self
|
|
36
|
+
|
|
37
|
+
def view(self, length: int) -> "ContributionView":
|
|
38
|
+
size = min(length, self.count)
|
|
39
|
+
chunks = []
|
|
40
|
+
remaining = size - len(self.tail)
|
|
41
|
+
head = self.head
|
|
42
|
+
while remaining > 0 and head is not None:
|
|
43
|
+
chunks.append(head.values)
|
|
44
|
+
head = head.prior
|
|
45
|
+
remaining -= _WIDTH
|
|
46
|
+
return ContributionView(self, tuple(chunks), size)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True, slots=True)
|
|
50
|
+
class ContributionView(Sequence[Value]):
|
|
51
|
+
"""A transient oldest-first sequence indexing only the requested chunks."""
|
|
52
|
+
|
|
53
|
+
history: ContributionHistory
|
|
54
|
+
chunks: tuple[tuple[Value, ...], ...]
|
|
55
|
+
size: int
|
|
56
|
+
|
|
57
|
+
def __len__(self):
|
|
58
|
+
return self.size
|
|
59
|
+
|
|
60
|
+
def __getitem__(self, index):
|
|
61
|
+
if isinstance(index, slice):
|
|
62
|
+
return [self[at] for at in range(*index.indices(self.size))]
|
|
63
|
+
if index < 0:
|
|
64
|
+
index += self.size
|
|
65
|
+
if index < 0 or index >= self.size:
|
|
66
|
+
raise IndexError(index)
|
|
67
|
+
back = self.size - 1 - index
|
|
68
|
+
tail = self.history.tail
|
|
69
|
+
if back < len(tail):
|
|
70
|
+
return tail[len(tail) - 1 - back]
|
|
71
|
+
before = back - len(tail)
|
|
72
|
+
return self.chunks[before // _WIDTH][_WIDTH - 1 - before % _WIDTH]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Exact two-argument hypotenuse, `stdlib.md` section 20.10.1."""
|
|
2
|
+
import math
|
|
3
|
+
import struct
|
|
4
|
+
|
|
5
|
+
_LEADING = 1 << 52
|
|
6
|
+
_FRACTION = _LEADING - 1
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _parts(value: float) -> tuple[int, int]:
|
|
10
|
+
encoded = struct.unpack('>Q', struct.pack('>d', abs(value)))[0]
|
|
11
|
+
exponent = encoded >> 52
|
|
12
|
+
significand = (encoded & _FRACTION) | (_LEADING if exponent else 0)
|
|
13
|
+
return significand, exponent - 1075 if exponent else -1074
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _decode(encoded: int) -> float:
|
|
17
|
+
return struct.unpack('>d', struct.pack('>Q', encoded))[0]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _integer_sqrt(value: int) -> int:
|
|
21
|
+
"""Monotone Newton steps from a power-of-two upper bound reach the floor."""
|
|
22
|
+
if value < 2:
|
|
23
|
+
return value
|
|
24
|
+
before = 1 << ((value.bit_length() + 1) // 2)
|
|
25
|
+
while True:
|
|
26
|
+
following = (before + value // before) // 2
|
|
27
|
+
if following >= before:
|
|
28
|
+
return before
|
|
29
|
+
before = following
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def hypotenuse(x: float, y: float) -> float | None:
|
|
33
|
+
"""Round the exact squared sum by comparing with a squared midpoint.
|
|
34
|
+
|
|
35
|
+
Binary64 input widths bound every temporary integer to fewer than 4,200 bits.
|
|
36
|
+
Encoding only after that decision avoids a second rounding through a scale.
|
|
37
|
+
"""
|
|
38
|
+
if not math.isfinite(x) or not math.isfinite(y):
|
|
39
|
+
return None
|
|
40
|
+
if x == 0:
|
|
41
|
+
return abs(y)
|
|
42
|
+
if y == 0:
|
|
43
|
+
return abs(x)
|
|
44
|
+
a, ea = _parts(x)
|
|
45
|
+
b, eb = _parts(y)
|
|
46
|
+
exponent = min(ea, eb)
|
|
47
|
+
square = (a * a << (2 * (ea - exponent))) + (b * b << (2 * (eb - exponent)))
|
|
48
|
+
spacing = max(-1074, exponent + (square.bit_length() - 1) // 2 - 52)
|
|
49
|
+
shift = exponent - spacing
|
|
50
|
+
numerator = square if shift < 0 else square << (2 * shift)
|
|
51
|
+
denominator = 1 << (-2 * shift) if shift < 0 else 1
|
|
52
|
+
significand = _integer_sqrt(numerator // denominator)
|
|
53
|
+
midpoint = 2 * significand + 1
|
|
54
|
+
comparison = 4 * numerator - denominator * midpoint * midpoint
|
|
55
|
+
if comparison > 0 or comparison == 0 and significand % 2:
|
|
56
|
+
significand += 1
|
|
57
|
+
if significand >= 2 * _LEADING:
|
|
58
|
+
significand >>= 1
|
|
59
|
+
spacing += 1
|
|
60
|
+
if significand < _LEADING:
|
|
61
|
+
return _decode(significand)
|
|
62
|
+
encoded_exponent = spacing + 1075
|
|
63
|
+
if encoded_exponent >= 2047:
|
|
64
|
+
return None
|
|
65
|
+
return _decode(encoded_exponent << 52 | (significand - _LEADING))
|