rbca 0.1.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.
rbca-0.1.0/.gitignore ADDED
@@ -0,0 +1,45 @@
1
+ # virtual environment
2
+ .venv/
3
+ .venv-test/
4
+
5
+ # Python cache
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+
10
+ # build
11
+ build/
12
+ dist/
13
+ *.egg-info/
14
+ archived_dist/
15
+
16
+ # test / coverage
17
+ .pytest_cache/
18
+ .coverage
19
+ htmlcov/
20
+
21
+ # type checker / linter
22
+ .mypy_cache/
23
+ .ruff_cache/
24
+
25
+ # Jupyter
26
+ .ipynb_checkpoints/
27
+
28
+ # IDE
29
+ .vscode/
30
+ .idea/
31
+
32
+ # OS
33
+ .DS_Store
34
+ Thumbs.db
35
+
36
+ # environment variables
37
+ .env
38
+ .env.*
39
+
40
+ # logs
41
+ *.log
42
+
43
+ uv.lock
44
+ test.py
45
+ /manual_tests
@@ -0,0 +1 @@
1
+ 3.13
rbca-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Imagination12357
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
rbca-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,203 @@
1
+ Metadata-Version: 2.4
2
+ Name: rbca
3
+ Version: 0.1.0
4
+ Summary: Recursive Binary Cosine Approximation
5
+ Project-URL: Homepage, https://github.com/Imagination12357/rbca
6
+ Project-URL: Repository, https://github.com/Imagination12357/rbca
7
+ Project-URL: Issues, https://github.com/Imagination12357/rbca/issues
8
+ Author: imagination12357
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: approximation,symbolic-math,sympy,trigonometry
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: sympy>=1.10.0
19
+ Description-Content-Type: text/markdown
20
+
21
+ # rbca
22
+
23
+ `rbca` (Recursive Binary Cosine Approximation) is a trigonometric approximation library with both symbolic and float APIs.
24
+
25
+ It computes `cos`, `sin`, and `tan` through recursive binary partitioning on angles.
26
+
27
+ The default APIs keep expressions in exact symbolic form (SymPy `Expr`) instead of forcing early floating-point evaluation. The `_float` APIs use float arithmetic internally for practical numeric calculation.
28
+
29
+ Current package version is `0.1.0`.
30
+
31
+ ## Features
32
+
33
+ - Symbolic trig outputs using SymPy expressions
34
+ - Practical float trig outputs using float arithmetic
35
+ - Recursive midpoint-based approximation for angle intervals
36
+ - Angle normalization over full `0~360` degree input range
37
+ - Three API families:
38
+ - `calc_*_pure`: mathematically pure symbolic recursion from `0` to `90`
39
+ - `calc_*`: symbolic recursion seeded with well-known anchor angles (`0, 15, 30, 45, 60, 75`)
40
+ - `calc_*_float`: practical float calculation using the same anchor strategy as `calc_*`
41
+
42
+ ## Installation
43
+
44
+ ### Using `pip`
45
+
46
+ ```bash
47
+ pip install rbca
48
+ ```
49
+
50
+ ### Local development (this repository)
51
+
52
+ ```bash
53
+ pip install -e .
54
+ ```
55
+
56
+ Project requirements:
57
+ - Python `>=3.10`
58
+ - `sympy>=1.10.0`
59
+
60
+ ## Quick Start
61
+
62
+ ```python
63
+ from rbca import calc_cos, calc_cos_float, calc_sin, calc_sin_float, calc_tan
64
+ from sympy import sqrtdenest
65
+
66
+ depth = 5
67
+
68
+ cos_expr = calc_cos(37, depth) # SymPy Expr
69
+ sin_expr = calc_sin(37, depth) # SymPy Expr
70
+ tan_expr = calc_tan(37, depth) # SymPy Expr
71
+
72
+ cos_value = calc_cos_float(37, depth) # float
73
+ sin_value = calc_sin_float(37, depth) # float
74
+
75
+ print(cos_expr) # symbolic expression
76
+ print(float(cos_expr)) # numeric projection
77
+ print(sqrtdenest(cos_expr)) # optional radical denesting
78
+ print(cos_value) # practical float result
79
+ ```
80
+
81
+ ## Choosing an API Family
82
+
83
+ - Use `calc_cos_pure`, `calc_sin_pure`, and `calc_tan_pure` when you want the mathematically pure symbolic path.
84
+ - This path starts from `[0, 90]` only.
85
+ - Use `calc_cos`, `calc_sin`, and `calc_tan` when you want symbolic results with a more practical anchored start.
86
+ - This path uses anchor angles (`0, 15, 30, 45, 60, 75, 90`) to start from a narrower interval.
87
+ - Use `calc_cos_float`, `calc_sin_float`, and `calc_tan_float` when you want practical float results.
88
+ - This path is implemented with float arithmetic internally.
89
+
90
+ ## Core API
91
+
92
+ - `calc_cos(angle, depth) -> Expr`
93
+ - `calc_sin(angle, depth) -> Expr`
94
+ - `calc_tan(angle, depth) -> Expr`
95
+
96
+ - `calc_cos_pure(angle, depth) -> Expr`
97
+ - `calc_sin_pure(angle, depth) -> Expr`
98
+ - `calc_tan_pure(angle, depth) -> Expr`
99
+
100
+ - `calc_cos_float(angle, depth) -> float`
101
+ - `calc_sin_float(angle, depth) -> float`
102
+ - `calc_tan_float(angle, depth) -> float`
103
+
104
+ For symbolic APIs, `angle` accepts `Rational | int | float | str` and is internally converted with `sympy.Rational` where needed.
105
+
106
+ Symbolic input note:
107
+ - `float` inputs are converted through `Rational(float_value)`, which can preserve binary-float artifacts.
108
+ - For exact decimal intent, prefer `str` or `Rational` directly.
109
+ - Example: use `"0.1"` instead of `0.1`.
110
+
111
+ For float APIs, `angle` accepts `int | float | str` and is internally converted with `float(angle)`.
112
+
113
+ `depth` is recursion depth:
114
+ - `depth = 0`: return one of the current interval endpoints
115
+ - larger depth: finer binary partitioning and generally tighter approximation
116
+
117
+ ## How It Works
118
+
119
+ 1. Normalize input angle to first quadrant (`0~90`) and track output sign.
120
+ 2. Choose initial bracket interval:
121
+ - `calc_cos_pure`: `[0, 90]`
122
+ - `calc_cos`: narrower interval between predefined anchor angles when possible.
123
+ - `calc_cos_float`: same anchored interval strategy as `calc_cos`, but with float nodes and float values.
124
+ 3. Recursively bisect the interval by midpoint angle.
125
+ 4. Compute midpoint cosine via:
126
+ - a product/half-angle style expression using endpoint cosine values.
127
+ 5. Recurse until `depth == 0` (or exact angle hit), then apply sign flip if needed.
128
+
129
+ ## Notes and Limitations
130
+
131
+ - This library is symbolic-first; expressions can grow quickly as depth increases.
132
+ - For angles where cosine is zero (e.g. `90 + 180k` degrees), `calc_tan` and `calc_tan_pure` return `sympy.zoo`.
133
+ - For the same tangent singularities, `calc_tan_float` lets Python's `ZeroDivisionError` propagate.
134
+ - Inputs with symbolic free variables are rejected during normalization.
135
+
136
+ ## Maths
137
+ Condition:
138
+
139
+ $$
140
+ 0^\circ \le A,B \le 90^\circ
141
+ $$
142
+
143
+ Using the half-angle formula,
144
+
145
+ $$
146
+ \cos\frac{A+B}{2}
147
+ =
148
+ \sqrt{\frac{1+\cos(A+B)}{2}}
149
+ $$
150
+
151
+ Also, by the angle addition formula,
152
+
153
+ $$
154
+ \cos(A+B)
155
+ =
156
+ \cos A\cos B-\sin A\sin B
157
+ $$
158
+
159
+ Since
160
+
161
+ $$
162
+ 0^\circ \le A,B \le 90^\circ
163
+ $$
164
+
165
+ we can use
166
+
167
+ $$
168
+ \sin A=\sqrt{1-\cos^2 A}
169
+ $$
170
+
171
+ and
172
+
173
+ $$
174
+ \sin B=\sqrt{1-\cos^2 B}
175
+ $$
176
+
177
+ Therefore,
178
+
179
+ $$
180
+ \cos(A+B)
181
+ =
182
+ \cos A\cos B
183
+ -
184
+ \sqrt{(1-\cos^2 A)(1-\cos^2 B)}
185
+ $$
186
+
187
+ Substituting this into the half-angle formula finally gives
188
+
189
+ $$
190
+ \boxed{
191
+ \cos\frac{A+B}{2}
192
+ =
193
+ \sqrt{
194
+ \frac{
195
+ 1+\cos A\cos B-\sqrt{(1-\cos^2 A)(1-\cos^2 B)}
196
+ }{2}
197
+ }
198
+ }
199
+ $$
200
+
201
+ ## License
202
+
203
+ MIT License. See [`LICENSE`](./LICENSE).
rbca-0.1.0/README.md ADDED
@@ -0,0 +1,183 @@
1
+ # rbca
2
+
3
+ `rbca` (Recursive Binary Cosine Approximation) is a trigonometric approximation library with both symbolic and float APIs.
4
+
5
+ It computes `cos`, `sin`, and `tan` through recursive binary partitioning on angles.
6
+
7
+ The default APIs keep expressions in exact symbolic form (SymPy `Expr`) instead of forcing early floating-point evaluation. The `_float` APIs use float arithmetic internally for practical numeric calculation.
8
+
9
+ Current package version is `0.1.0`.
10
+
11
+ ## Features
12
+
13
+ - Symbolic trig outputs using SymPy expressions
14
+ - Practical float trig outputs using float arithmetic
15
+ - Recursive midpoint-based approximation for angle intervals
16
+ - Angle normalization over full `0~360` degree input range
17
+ - Three API families:
18
+ - `calc_*_pure`: mathematically pure symbolic recursion from `0` to `90`
19
+ - `calc_*`: symbolic recursion seeded with well-known anchor angles (`0, 15, 30, 45, 60, 75`)
20
+ - `calc_*_float`: practical float calculation using the same anchor strategy as `calc_*`
21
+
22
+ ## Installation
23
+
24
+ ### Using `pip`
25
+
26
+ ```bash
27
+ pip install rbca
28
+ ```
29
+
30
+ ### Local development (this repository)
31
+
32
+ ```bash
33
+ pip install -e .
34
+ ```
35
+
36
+ Project requirements:
37
+ - Python `>=3.10`
38
+ - `sympy>=1.10.0`
39
+
40
+ ## Quick Start
41
+
42
+ ```python
43
+ from rbca import calc_cos, calc_cos_float, calc_sin, calc_sin_float, calc_tan
44
+ from sympy import sqrtdenest
45
+
46
+ depth = 5
47
+
48
+ cos_expr = calc_cos(37, depth) # SymPy Expr
49
+ sin_expr = calc_sin(37, depth) # SymPy Expr
50
+ tan_expr = calc_tan(37, depth) # SymPy Expr
51
+
52
+ cos_value = calc_cos_float(37, depth) # float
53
+ sin_value = calc_sin_float(37, depth) # float
54
+
55
+ print(cos_expr) # symbolic expression
56
+ print(float(cos_expr)) # numeric projection
57
+ print(sqrtdenest(cos_expr)) # optional radical denesting
58
+ print(cos_value) # practical float result
59
+ ```
60
+
61
+ ## Choosing an API Family
62
+
63
+ - Use `calc_cos_pure`, `calc_sin_pure`, and `calc_tan_pure` when you want the mathematically pure symbolic path.
64
+ - This path starts from `[0, 90]` only.
65
+ - Use `calc_cos`, `calc_sin`, and `calc_tan` when you want symbolic results with a more practical anchored start.
66
+ - This path uses anchor angles (`0, 15, 30, 45, 60, 75, 90`) to start from a narrower interval.
67
+ - Use `calc_cos_float`, `calc_sin_float`, and `calc_tan_float` when you want practical float results.
68
+ - This path is implemented with float arithmetic internally.
69
+
70
+ ## Core API
71
+
72
+ - `calc_cos(angle, depth) -> Expr`
73
+ - `calc_sin(angle, depth) -> Expr`
74
+ - `calc_tan(angle, depth) -> Expr`
75
+
76
+ - `calc_cos_pure(angle, depth) -> Expr`
77
+ - `calc_sin_pure(angle, depth) -> Expr`
78
+ - `calc_tan_pure(angle, depth) -> Expr`
79
+
80
+ - `calc_cos_float(angle, depth) -> float`
81
+ - `calc_sin_float(angle, depth) -> float`
82
+ - `calc_tan_float(angle, depth) -> float`
83
+
84
+ For symbolic APIs, `angle` accepts `Rational | int | float | str` and is internally converted with `sympy.Rational` where needed.
85
+
86
+ Symbolic input note:
87
+ - `float` inputs are converted through `Rational(float_value)`, which can preserve binary-float artifacts.
88
+ - For exact decimal intent, prefer `str` or `Rational` directly.
89
+ - Example: use `"0.1"` instead of `0.1`.
90
+
91
+ For float APIs, `angle` accepts `int | float | str` and is internally converted with `float(angle)`.
92
+
93
+ `depth` is recursion depth:
94
+ - `depth = 0`: return one of the current interval endpoints
95
+ - larger depth: finer binary partitioning and generally tighter approximation
96
+
97
+ ## How It Works
98
+
99
+ 1. Normalize input angle to first quadrant (`0~90`) and track output sign.
100
+ 2. Choose initial bracket interval:
101
+ - `calc_cos_pure`: `[0, 90]`
102
+ - `calc_cos`: narrower interval between predefined anchor angles when possible.
103
+ - `calc_cos_float`: same anchored interval strategy as `calc_cos`, but with float nodes and float values.
104
+ 3. Recursively bisect the interval by midpoint angle.
105
+ 4. Compute midpoint cosine via:
106
+ - a product/half-angle style expression using endpoint cosine values.
107
+ 5. Recurse until `depth == 0` (or exact angle hit), then apply sign flip if needed.
108
+
109
+ ## Notes and Limitations
110
+
111
+ - This library is symbolic-first; expressions can grow quickly as depth increases.
112
+ - For angles where cosine is zero (e.g. `90 + 180k` degrees), `calc_tan` and `calc_tan_pure` return `sympy.zoo`.
113
+ - For the same tangent singularities, `calc_tan_float` lets Python's `ZeroDivisionError` propagate.
114
+ - Inputs with symbolic free variables are rejected during normalization.
115
+
116
+ ## Maths
117
+ Condition:
118
+
119
+ $$
120
+ 0^\circ \le A,B \le 90^\circ
121
+ $$
122
+
123
+ Using the half-angle formula,
124
+
125
+ $$
126
+ \cos\frac{A+B}{2}
127
+ =
128
+ \sqrt{\frac{1+\cos(A+B)}{2}}
129
+ $$
130
+
131
+ Also, by the angle addition formula,
132
+
133
+ $$
134
+ \cos(A+B)
135
+ =
136
+ \cos A\cos B-\sin A\sin B
137
+ $$
138
+
139
+ Since
140
+
141
+ $$
142
+ 0^\circ \le A,B \le 90^\circ
143
+ $$
144
+
145
+ we can use
146
+
147
+ $$
148
+ \sin A=\sqrt{1-\cos^2 A}
149
+ $$
150
+
151
+ and
152
+
153
+ $$
154
+ \sin B=\sqrt{1-\cos^2 B}
155
+ $$
156
+
157
+ Therefore,
158
+
159
+ $$
160
+ \cos(A+B)
161
+ =
162
+ \cos A\cos B
163
+ -
164
+ \sqrt{(1-\cos^2 A)(1-\cos^2 B)}
165
+ $$
166
+
167
+ Substituting this into the half-angle formula finally gives
168
+
169
+ $$
170
+ \boxed{
171
+ \cos\frac{A+B}{2}
172
+ =
173
+ \sqrt{
174
+ \frac{
175
+ 1+\cos A\cos B-\sqrt{(1-\cos^2 A)(1-\cos^2 B)}
176
+ }{2}
177
+ }
178
+ }
179
+ $$
180
+
181
+ ## License
182
+
183
+ MIT License. See [`LICENSE`](./LICENSE).
@@ -0,0 +1,38 @@
1
+ [project]
2
+ name = "rbca"
3
+ version = "0.1.0"
4
+ description = "Recursive Binary Cosine Approximation"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { text = "MIT" }
8
+ authors = [
9
+ { name = "imagination12357" },
10
+ ]
11
+ keywords = ["symbolic-math", "trigonometry", "sympy", "approximation"]
12
+ classifiers = [
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.10",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ "Topic :: Scientific/Engineering :: Mathematics",
18
+ ]
19
+ dependencies = [
20
+ "sympy>=1.10.0",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/Imagination12357/rbca"
25
+ Repository = "https://github.com/Imagination12357/rbca"
26
+ Issues = "https://github.com/Imagination12357/rbca/issues"
27
+
28
+ [dependency-groups]
29
+ dev = [
30
+ "pytest>=8,<9"
31
+ ]
32
+
33
+ [build-system]
34
+ requires = ["hatchling"]
35
+ build-backend = "hatchling.build"
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["src/rbca"]
@@ -0,0 +1,2 @@
1
+ from .lib import calc_cos, calc_cos_pure, calc_sin, calc_sin_pure, calc_tan, calc_tan_pure
2
+ from .lib_float import calc_cos_float, calc_sin_float, calc_tan_float
@@ -0,0 +1,48 @@
1
+ from sympy import Rational, Expr, sqrt
2
+ from dataclasses import dataclass
3
+
4
+ @dataclass
5
+ class RbcaNode:
6
+ angle: Rational
7
+ value: Expr
8
+
9
+ def __neg__(self):
10
+ return RbcaNode(self.angle, -self.value)
11
+
12
+ def flipif(self, is_flipped: bool):
13
+ return -self if is_flipped else self
14
+
15
+ def calc_mid_cos(lo: RbcaNode, hi:RbcaNode):
16
+ return RbcaNode(
17
+ (lo.angle + hi.angle) / 2,
18
+ #(sqrt((1+lo.value)*(1+hi.value))-sqrt((1-lo.value)*(1-hi.value)))/2
19
+ sqrt(
20
+ (1 + lo.value*hi.value - sqrt(1 - lo.value**2)*sqrt(1 - hi.value**2)) / 2
21
+ )
22
+ )
23
+
24
+ def _validate_depth(depth: int):
25
+
26
+ if not isinstance(depth, int) or isinstance(depth, bool):
27
+ raise TypeError("Depth must be integer.")
28
+
29
+ if depth < 0:
30
+ raise ValueError("Depth must be non-negative.")
31
+
32
+ def recursive_approx_cos(lo: RbcaNode, hi:RbcaNode, target:Rational, depth:int) -> RbcaNode:
33
+
34
+ _validate_depth(depth)
35
+
36
+ if depth == 0:
37
+ return lo if target - lo.angle < hi.angle - target else hi
38
+
39
+ mid = calc_mid_cos(lo, hi)
40
+
41
+ if mid.angle == target:
42
+ return mid
43
+
44
+ if target < mid.angle:
45
+ return recursive_approx_cos(lo, mid, target, depth-1)
46
+
47
+ else:
48
+ return recursive_approx_cos(mid, hi, target, depth-1)
@@ -0,0 +1,41 @@
1
+ import math
2
+ from dataclasses import dataclass
3
+ from .core import _validate_depth
4
+
5
+ @dataclass
6
+ class RbcaNodeF:
7
+ angle: float
8
+ value: float
9
+
10
+ def __neg__(self):
11
+ return RbcaNodeF(self.angle, -self.value)
12
+
13
+ def flipif(self, is_flipped: bool):
14
+ return -self if is_flipped else self
15
+
16
+ def calc_mid_cos_f(lo: RbcaNodeF, hi:RbcaNodeF):
17
+ return RbcaNodeF(
18
+ (lo.angle + hi.angle) / 2,
19
+ (math.sqrt((1+lo.value)*(1+hi.value))-math.sqrt((1-lo.value)*(1-hi.value)))/2
20
+ #math.sqrt(
21
+ # (1 + lo.value*hi.value - math.sqrt(1 - lo.value**2)*math.sqrt(1 - hi.value**2)) / 2
22
+ #)
23
+ )
24
+
25
+ def recursive_approx_cos_f(lo: RbcaNodeF, hi:RbcaNodeF, target:float, depth:int) -> RbcaNodeF:
26
+
27
+ _validate_depth(depth)
28
+
29
+ if depth == 0:
30
+ return lo if target - lo.angle < hi.angle - target else hi
31
+
32
+ mid = calc_mid_cos_f(lo, hi)
33
+
34
+ if mid.angle == target:
35
+ return mid
36
+
37
+ if target < mid.angle:
38
+ return recursive_approx_cos_f(lo, mid, target, depth-1)
39
+
40
+ else:
41
+ return recursive_approx_cos_f(mid, hi, target, depth-1)
@@ -0,0 +1,146 @@
1
+ from sympy import Rational, Expr, sqrt, zoo
2
+ from .core import RbcaNode, recursive_approx_cos, _validate_depth
3
+
4
+ def _rationalize(angle:Rational|int|float|str) -> Rational:
5
+ try:
6
+ return Rational(angle)
7
+ except Exception as e:
8
+ raise ValueError("Angle must be representable as a single Rational.") from e
9
+
10
+ def normalize_cos(angle:Rational|int|float|str) -> tuple[Rational, bool]:
11
+
12
+ norm_angle = _rationalize(angle) % 360
13
+
14
+ if norm_angle <= 90:
15
+ final_angle = norm_angle
16
+ is_flipped = False
17
+
18
+ elif norm_angle <= 180:
19
+ final_angle = 180 - norm_angle
20
+ is_flipped = True
21
+
22
+ elif norm_angle <= 270:
23
+ final_angle = norm_angle - 180
24
+ is_flipped = True
25
+
26
+ else:
27
+ final_angle = 360 - norm_angle
28
+ is_flipped = False
29
+
30
+ return final_angle, is_flipped
31
+
32
+ def calc_cos_pure(angle:Rational|int|float|str, depth:int) -> Expr:
33
+
34
+ _validate_depth(depth)
35
+
36
+ norm_angle, is_flipped = normalize_cos(angle)
37
+
38
+ return recursive_approx_cos(
39
+ RbcaNode(Rational(0), 1),
40
+ RbcaNode(Rational(90), 0),
41
+ norm_angle,
42
+ depth
43
+ ).flipif(is_flipped).value
44
+
45
+ def calc_sin_pure(angle:Rational|int|float|str, depth:int) -> Expr:
46
+ return calc_cos_pure(90 - _rationalize(angle), depth)
47
+
48
+ def calc_tan_pure(angle:Rational|int|float|str, depth:int) -> Expr:
49
+ try:
50
+ return calc_sin_pure(angle, depth) / calc_cos_pure(angle, depth)
51
+ except ZeroDivisionError:
52
+ return zoo
53
+
54
+
55
+ def calc_cos(angle:Rational|int|float|str, depth:int) -> Expr:
56
+
57
+ _validate_depth(depth)
58
+
59
+ norm_angle, is_flipped = normalize_cos(angle)
60
+
61
+ if norm_angle == 0:
62
+ return RbcaNode(Rational(0), Rational(1)).flipif(is_flipped).value
63
+
64
+ elif norm_angle < 15:
65
+ return recursive_approx_cos(
66
+ RbcaNode(Rational(0), Rational(1)),
67
+ RbcaNode(Rational(15), sqrt(6)/4 + sqrt(2)/4),
68
+ norm_angle,
69
+ depth
70
+ ).flipif(is_flipped).value
71
+
72
+ elif norm_angle == 15:
73
+ return RbcaNode(Rational(15), sqrt(6)/4 + sqrt(2)/4).flipif(is_flipped).value
74
+
75
+ elif norm_angle < 30:
76
+ return recursive_approx_cos(
77
+ RbcaNode(Rational(15), sqrt(6)/4 + sqrt(2)/4),
78
+ RbcaNode(Rational(30), sqrt(3)/2),
79
+ norm_angle,
80
+ depth
81
+ ).flipif(is_flipped).value
82
+
83
+ elif norm_angle == 30:
84
+ return RbcaNode(Rational(30), sqrt(3)/2).flipif(is_flipped).value
85
+
86
+ elif norm_angle < 45:
87
+ return recursive_approx_cos(
88
+ RbcaNode(Rational(30), sqrt(3)/2),
89
+ RbcaNode(Rational(45), sqrt(2)/2),
90
+ norm_angle,
91
+ depth
92
+ ).flipif(is_flipped).value
93
+
94
+ elif norm_angle == 45:
95
+ return RbcaNode(Rational(45), sqrt(2)/2).flipif(is_flipped).value
96
+
97
+ elif norm_angle < 60:
98
+ return recursive_approx_cos(
99
+ RbcaNode(Rational(45), sqrt(2)/2),
100
+ RbcaNode(Rational(60), Rational(1,2)),
101
+ norm_angle,
102
+ depth
103
+ ).flipif(is_flipped).value
104
+
105
+ elif norm_angle == 60:
106
+ return RbcaNode(Rational(60), Rational(1,2)).flipif(is_flipped).value
107
+
108
+ elif norm_angle < 75:
109
+ return recursive_approx_cos(
110
+ RbcaNode(Rational(60), Rational(1,2)),
111
+ RbcaNode(Rational(75), sqrt(6)/4 - sqrt(2)/4),
112
+ norm_angle,
113
+ depth
114
+ ).flipif(is_flipped).value
115
+
116
+ elif norm_angle == 75:
117
+ return RbcaNode(Rational(75), sqrt(6)/4 - sqrt(2)/4).flipif(is_flipped).value
118
+
119
+ elif norm_angle < 90:
120
+ return recursive_approx_cos(
121
+ RbcaNode(Rational(75), sqrt(6)/4 - sqrt(2)/4),
122
+ RbcaNode(Rational(90), 0),
123
+ norm_angle,
124
+ depth
125
+ ).flipif(is_flipped).value
126
+
127
+ elif norm_angle == 90:
128
+ return RbcaNode(Rational(90), 0).flipif(is_flipped).value
129
+
130
+ else:
131
+ raise RuntimeError(f"Normalization is wrong with something\nnorm_angle:{norm_angle}")
132
+
133
+ def calc_sin(angle:Rational|int|float|str, depth:int) -> Expr:
134
+ return calc_cos(90 - _rationalize(angle), depth)
135
+
136
+ def calc_tan(angle:Rational|int|float|str, depth:int) -> Expr:
137
+ try:
138
+ return calc_sin(angle, depth) / calc_cos(angle, depth)
139
+ except ZeroDivisionError:
140
+ return zoo
141
+
142
+ if __name__ == "__main__":
143
+ from sympy import latex, sqrtdenest
144
+ result = sqrtdenest(calc_cos(37, 5))
145
+ print(latex(result))
146
+ print(float(result))
@@ -0,0 +1,111 @@
1
+ from .core_float import RbcaNodeF, recursive_approx_cos_f, _validate_depth
2
+ import math
3
+
4
+ def normalize_cos(angle:int|float|str) -> tuple[float, bool]:
5
+
6
+ norm_angle = float(angle) % 360
7
+
8
+ if norm_angle <= 90:
9
+ final_angle = norm_angle
10
+ is_flipped = False
11
+
12
+ elif norm_angle <= 180:
13
+ final_angle = 180 - norm_angle
14
+ is_flipped = True
15
+
16
+ elif norm_angle <= 270:
17
+ final_angle = norm_angle - 180
18
+ is_flipped = True
19
+
20
+ else:
21
+ final_angle = 360 - norm_angle
22
+ is_flipped = False
23
+
24
+ return final_angle, is_flipped
25
+
26
+ def calc_cos_float(angle:int|float|str, depth:int) -> float:
27
+
28
+ _validate_depth(depth)
29
+
30
+ norm_angle, is_flipped = normalize_cos(angle)
31
+
32
+ if norm_angle == 0:
33
+ return RbcaNodeF(0, 1).flipif(is_flipped).value
34
+
35
+ elif norm_angle < 15:
36
+ return recursive_approx_cos_f(
37
+ RbcaNodeF(0, 1),
38
+ RbcaNodeF(15, math.sqrt(6)/4 + math.sqrt(2)/4),
39
+ norm_angle,
40
+ depth
41
+ ).flipif(is_flipped).value
42
+
43
+ elif norm_angle == 15:
44
+ return RbcaNodeF(15, math.sqrt(6)/4 + math.sqrt(2)/4).flipif(is_flipped).value
45
+
46
+ elif norm_angle < 30:
47
+ return recursive_approx_cos_f(
48
+ RbcaNodeF(15, math.sqrt(6)/4 + math.sqrt(2)/4),
49
+ RbcaNodeF(30, math.sqrt(3)/2),
50
+ norm_angle,
51
+ depth
52
+ ).flipif(is_flipped).value
53
+
54
+ elif norm_angle == 30:
55
+ return RbcaNodeF(30, math.sqrt(3)/2).flipif(is_flipped).value
56
+
57
+ elif norm_angle < 45:
58
+ return recursive_approx_cos_f(
59
+ RbcaNodeF(30, math.sqrt(3)/2),
60
+ RbcaNodeF(45, math.sqrt(2)/2),
61
+ norm_angle,
62
+ depth
63
+ ).flipif(is_flipped).value
64
+
65
+ elif norm_angle == 45:
66
+ return RbcaNodeF(45, math.sqrt(2)/2).flipif(is_flipped).value
67
+
68
+ elif norm_angle < 60:
69
+ return recursive_approx_cos_f(
70
+ RbcaNodeF(45, math.sqrt(2)/2),
71
+ RbcaNodeF(60, 1/2),
72
+ norm_angle,
73
+ depth
74
+ ).flipif(is_flipped).value
75
+
76
+ elif norm_angle == 60:
77
+ return RbcaNodeF(60, 1/2).flipif(is_flipped).value
78
+
79
+ elif norm_angle < 75:
80
+ return recursive_approx_cos_f(
81
+ RbcaNodeF(60, 1/2),
82
+ RbcaNodeF(75, math.sqrt(6)/4 - math.sqrt(2)/4),
83
+ norm_angle,
84
+ depth
85
+ ).flipif(is_flipped).value
86
+
87
+ elif norm_angle == 75:
88
+ return RbcaNodeF(75, math.sqrt(6)/4 - math.sqrt(2)/4).flipif(is_flipped).value
89
+
90
+ elif norm_angle < 90:
91
+ return recursive_approx_cos_f(
92
+ RbcaNodeF(75, math.sqrt(6)/4 - math.sqrt(2)/4),
93
+ RbcaNodeF(90, 0),
94
+ norm_angle,
95
+ depth
96
+ ).flipif(is_flipped).value
97
+
98
+ elif norm_angle == 90:
99
+ return RbcaNodeF(90, 0).flipif(is_flipped).value
100
+
101
+ else:
102
+ raise RuntimeError(f"Normalization is wrong with something\nnorm_angle:{norm_angle}")
103
+
104
+ def calc_sin_float(angle:int|float|str, depth:int) -> float:
105
+ return calc_cos_float(90 - float(angle), depth)
106
+
107
+ def calc_tan_float(angle:int|float|str, depth:int) -> float:
108
+ return calc_sin_float(angle, depth) / calc_cos_float(angle, depth) # Let the ZeroDivisionError go
109
+
110
+ if __name__ == "__main__":
111
+ print(calc_cos_float(37,10))
@@ -0,0 +1,132 @@
1
+ import pytest
2
+ from sympy import Rational, sqrt, zoo
3
+
4
+ from rbca import (
5
+ calc_cos,
6
+ calc_cos_float,
7
+ calc_sin,
8
+ calc_sin_float,
9
+ calc_tan,
10
+ calc_tan_float,
11
+ calc_tan_pure,
12
+ )
13
+ from rbca.lib import normalize_cos
14
+
15
+
16
+ @pytest.mark.parametrize(
17
+ ("angle", "expected"),
18
+ [
19
+ (0, (Rational(0), False)),
20
+ (90, (Rational(90), False)),
21
+ (180, (Rational(0), True)),
22
+ (270, (Rational(90), True)),
23
+ (360, (Rational(0), False)),
24
+ (-90, (Rational(90), True)),
25
+ ],
26
+ )
27
+ def test_normalize_cos_boundaries(angle, expected):
28
+ assert normalize_cos(angle) == expected
29
+
30
+
31
+ @pytest.mark.parametrize(
32
+ ("angle", "expected"),
33
+ [
34
+ (0, Rational(1)),
35
+ (30, sqrt(3) / 2),
36
+ (45, sqrt(2) / 2),
37
+ (60, Rational(1, 2)),
38
+ (90, Rational(0)),
39
+ ],
40
+ )
41
+ def test_calc_cos_anchor_values(angle, expected):
42
+ assert calc_cos(angle, 2) == expected
43
+
44
+
45
+ @pytest.mark.parametrize(
46
+ ("angle", "expected"),
47
+ [
48
+ (0, Rational(0)),
49
+ (30, Rational(1, 2)),
50
+ (45, sqrt(2) / 2),
51
+ (60, sqrt(3) / 2),
52
+ (90, Rational(1)),
53
+ ],
54
+ )
55
+ def test_calc_sin_anchor_values(angle, expected):
56
+ assert calc_sin(angle, 2) == expected
57
+
58
+
59
+ def test_depth_negative_raises():
60
+ with pytest.raises(ValueError):
61
+ calc_cos(30, -1)
62
+
63
+
64
+ def test_depth_non_integer_raises():
65
+ with pytest.raises(TypeError):
66
+ calc_cos(30, 1.5)
67
+
68
+
69
+ def test_depth_bool_raises():
70
+ with pytest.raises(TypeError):
71
+ calc_cos(30, True)
72
+
73
+
74
+ def test_calc_tan_90_behavior():
75
+ assert calc_tan(90, 2) == zoo
76
+
77
+
78
+ def test_calc_tan_pure_90_behavior():
79
+ assert calc_tan_pure(90, 2) == zoo
80
+
81
+
82
+ @pytest.mark.parametrize(
83
+ ("angle", "expected"),
84
+ [
85
+ (0, 1.0),
86
+ (30, 3**0.5 / 2),
87
+ (45, 2**0.5 / 2),
88
+ (60, 0.5),
89
+ (90, 0.0),
90
+ ],
91
+ )
92
+ def test_calc_cos_float_anchor_values(angle, expected):
93
+ assert calc_cos_float(angle, 2) == pytest.approx(expected)
94
+
95
+
96
+ @pytest.mark.parametrize(
97
+ ("angle", "expected"),
98
+ [
99
+ (0, 0.0),
100
+ (30, 0.5),
101
+ (45, 2**0.5 / 2),
102
+ (60, 3**0.5 / 2),
103
+ (90, 1.0),
104
+ ],
105
+ )
106
+ def test_calc_sin_float_anchor_values(angle, expected):
107
+ assert calc_sin_float(angle, 2) == pytest.approx(expected)
108
+
109
+
110
+ def test_calc_tan_float_90_behavior():
111
+ with pytest.raises(ZeroDivisionError):
112
+ calc_tan_float(90, 2)
113
+
114
+
115
+ def test_calc_cos_float_depth_negative_raises():
116
+ with pytest.raises(ValueError):
117
+ calc_cos_float(30, -1)
118
+
119
+
120
+ def test_calc_cos_float_depth_non_integer_raises():
121
+ with pytest.raises(TypeError):
122
+ calc_cos_float(30, 1.5)
123
+
124
+
125
+ def test_calc_cos_float_angle_non_floatable_raises():
126
+ with pytest.raises(ValueError):
127
+ calc_cos_float("Not-a-float", 2)
128
+
129
+
130
+ def test_angle_non_Rational_raises():
131
+ with pytest.raises(ValueError):
132
+ calc_cos("Not-a-Rational", 2)