celestial-astrology 0.1.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.
- astrology/__init__.py +112 -0
- astrology/_distribution.py +17 -0
- astrology/_validation.py +16 -0
- astrology/aspects.py +289 -0
- astrology/birth.py +369 -0
- astrology/celestial.py +178 -0
- astrology/chart.py +391 -0
- astrology/config.py +253 -0
- astrology/houses.py +73 -0
- astrology/knowledge/__init__.py +32 -0
- astrology/knowledge/catalog.json +78 -0
- astrology/knowledge/catalog.py +369 -0
- astrology/knowledge/records.py +337 -0
- astrology/py.typed +0 -0
- astrology/serialization.py +520 -0
- astrology/text.py +263 -0
- astrology/zodiac.py +92 -0
- celestial_astrology-0.1.0.dist-info/METADATA +220 -0
- celestial_astrology-0.1.0.dist-info/RECORD +22 -0
- celestial_astrology-0.1.0.dist-info/WHEEL +5 -0
- celestial_astrology-0.1.0.dist-info/licenses/LICENSE +21 -0
- celestial_astrology-0.1.0.dist-info/top_level.txt +1 -0
astrology/__init__.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Birth inputs, requested configuration, chart composition and independent knowledge.
|
|
2
|
+
|
|
3
|
+
Importing the package initializes no native astronomy.
|
|
4
|
+
|
|
5
|
+
出生输入、请求配置、命盘组合,以及独立的知识查询;导入这个包本身不会初始化原生天文库。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from astrology.aspects import (
|
|
9
|
+
AspectHit,
|
|
10
|
+
AspectMotion,
|
|
11
|
+
AspectObject,
|
|
12
|
+
aspect_angle,
|
|
13
|
+
find_aspects,
|
|
14
|
+
)
|
|
15
|
+
from astrology.birth import (
|
|
16
|
+
AmbiguousBirthTimeError,
|
|
17
|
+
BirthRecord,
|
|
18
|
+
FixedOffset,
|
|
19
|
+
HistoricalTimeError,
|
|
20
|
+
IanaTimezone,
|
|
21
|
+
Location,
|
|
22
|
+
MissingBirthDataError,
|
|
23
|
+
NonexistentBirthTimeError,
|
|
24
|
+
ResolvedBirth,
|
|
25
|
+
TimePrecision,
|
|
26
|
+
prepare_natal_input,
|
|
27
|
+
resolve_birth_time,
|
|
28
|
+
)
|
|
29
|
+
from astrology.chart import (
|
|
30
|
+
ChartDomainError,
|
|
31
|
+
ChartHouses,
|
|
32
|
+
ChartProvenance,
|
|
33
|
+
ChartTimes,
|
|
34
|
+
EclipticPosition,
|
|
35
|
+
EffectiveConfiguration,
|
|
36
|
+
NatalChart,
|
|
37
|
+
Placement,
|
|
38
|
+
natal_chart,
|
|
39
|
+
)
|
|
40
|
+
from astrology.config import (
|
|
41
|
+
AspectRule,
|
|
42
|
+
Body,
|
|
43
|
+
ChartConfig,
|
|
44
|
+
HouseSystem,
|
|
45
|
+
LunarNode,
|
|
46
|
+
MajorAspect,
|
|
47
|
+
NodeDefinition,
|
|
48
|
+
ObjectOrb,
|
|
49
|
+
OrbCombination,
|
|
50
|
+
)
|
|
51
|
+
from astrology.houses import house_of
|
|
52
|
+
from astrology.knowledge import Catalog, load_catalog
|
|
53
|
+
from astrology.serialization import (
|
|
54
|
+
ChartRecordError,
|
|
55
|
+
ChartReplayError,
|
|
56
|
+
dumps_chart,
|
|
57
|
+
loads_chart,
|
|
58
|
+
recompute_chart,
|
|
59
|
+
)
|
|
60
|
+
from astrology.text import format_chart
|
|
61
|
+
from astrology.zodiac import Sign, sign_degree, sign_of, truncate_display
|
|
62
|
+
|
|
63
|
+
__all__ = [
|
|
64
|
+
"AmbiguousBirthTimeError",
|
|
65
|
+
"AspectHit",
|
|
66
|
+
"AspectMotion",
|
|
67
|
+
"AspectObject",
|
|
68
|
+
"AspectRule",
|
|
69
|
+
"BirthRecord",
|
|
70
|
+
"Body",
|
|
71
|
+
"Catalog",
|
|
72
|
+
"ChartConfig",
|
|
73
|
+
"ChartDomainError",
|
|
74
|
+
"ChartHouses",
|
|
75
|
+
"ChartProvenance",
|
|
76
|
+
"ChartRecordError",
|
|
77
|
+
"ChartReplayError",
|
|
78
|
+
"ChartTimes",
|
|
79
|
+
"EclipticPosition",
|
|
80
|
+
"EffectiveConfiguration",
|
|
81
|
+
"FixedOffset",
|
|
82
|
+
"HistoricalTimeError",
|
|
83
|
+
"HouseSystem",
|
|
84
|
+
"IanaTimezone",
|
|
85
|
+
"Location",
|
|
86
|
+
"LunarNode",
|
|
87
|
+
"MajorAspect",
|
|
88
|
+
"MissingBirthDataError",
|
|
89
|
+
"NatalChart",
|
|
90
|
+
"NodeDefinition",
|
|
91
|
+
"NonexistentBirthTimeError",
|
|
92
|
+
"ObjectOrb",
|
|
93
|
+
"OrbCombination",
|
|
94
|
+
"Placement",
|
|
95
|
+
"ResolvedBirth",
|
|
96
|
+
"Sign",
|
|
97
|
+
"TimePrecision",
|
|
98
|
+
"aspect_angle",
|
|
99
|
+
"dumps_chart",
|
|
100
|
+
"find_aspects",
|
|
101
|
+
"format_chart",
|
|
102
|
+
"house_of",
|
|
103
|
+
"load_catalog",
|
|
104
|
+
"loads_chart",
|
|
105
|
+
"natal_chart",
|
|
106
|
+
"prepare_natal_input",
|
|
107
|
+
"recompute_chart",
|
|
108
|
+
"resolve_birth_time",
|
|
109
|
+
"sign_degree",
|
|
110
|
+
"sign_of",
|
|
111
|
+
"truncate_display",
|
|
112
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Installed distribution identity, separate from the Python import name.
|
|
2
|
+
|
|
3
|
+
已安装分发包的身份,与 Python 导入名分开。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from importlib.metadata import version
|
|
7
|
+
from typing import Final
|
|
8
|
+
|
|
9
|
+
DISTRIBUTION_NAME: Final = "celestial-astrology"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def installed_version() -> str:
|
|
13
|
+
"""Read installed metadata explicitly; missing metadata raises PackageNotFoundError.
|
|
14
|
+
|
|
15
|
+
显式读取已安装的元数据;缺失时抛出 PackageNotFoundError。
|
|
16
|
+
"""
|
|
17
|
+
return version(DISTRIBUTION_NAME)
|
astrology/_validation.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Numeric input checks shared by the owning domains. 各领域共用的数值输入检查。"""
|
|
2
|
+
|
|
3
|
+
from math import isfinite
|
|
4
|
+
from typing import cast
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def finite_number(value: object, name: str) -> float:
|
|
8
|
+
if type(value) not in (int, float):
|
|
9
|
+
raise TypeError(f"{name} must be a number, not bool or a converted string")
|
|
10
|
+
try:
|
|
11
|
+
number = float(cast("int | float", value))
|
|
12
|
+
except OverflowError as exc:
|
|
13
|
+
raise ValueError(f"{name} must be finite") from exc
|
|
14
|
+
if not isfinite(number):
|
|
15
|
+
raise ValueError(f"{name} must be finite")
|
|
16
|
+
return number
|
astrology/aspects.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Find major aspects from longitudes and rates at a given instant.
|
|
2
|
+
|
|
3
|
+
Each result records the angle measured from the first object to the second, the
|
|
4
|
+
exact aspect angle used for comparison (the branch), and the signed difference.
|
|
5
|
+
The motion status tells whether the magnitude of that difference is shrinking or growing.
|
|
6
|
+
It does not predict later changes in motion or whether the aspect will become
|
|
7
|
+
exact. This module needs no chart and does not call the astronomy library.
|
|
8
|
+
|
|
9
|
+
根据同一时刻的黄经和速率判断主要相位。每条结果记录从第一个对象到第二个对象的角度、
|
|
10
|
+
用来对照的精确相位角(分支),以及两者的带符号偏差。运动状态说明偏差的绝对值此刻在缩小还是增大,
|
|
11
|
+
不预测之后的运动变化,也不预测相位之后会不会变得精确。本模块不需要命盘,也不调用天文库。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
from collections.abc import Sequence
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from enum import StrEnum
|
|
20
|
+
from math import remainder
|
|
21
|
+
|
|
22
|
+
from astrology._validation import finite_number
|
|
23
|
+
from astrology.config import AspectRule, Body, LunarNode, MajorAspect, OrbCombination
|
|
24
|
+
|
|
25
|
+
_HALF_MAXIMUM = sys.float_info.max / 2.0
|
|
26
|
+
|
|
27
|
+
_ANGLES: dict[MajorAspect, float] = {
|
|
28
|
+
MajorAspect.CONJUNCTION: 0.0,
|
|
29
|
+
MajorAspect.SEXTILE: 60.0,
|
|
30
|
+
MajorAspect.SQUARE: 90.0,
|
|
31
|
+
MajorAspect.TRINE: 120.0,
|
|
32
|
+
MajorAspect.OPPOSITION: 180.0,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AspectMotion(StrEnum):
|
|
37
|
+
"""Whether an aspect is approaching or moving away from its exact angle at this instant.
|
|
38
|
+
|
|
39
|
+
The relative longitude rate is the second object's rate minus the first's. Away
|
|
40
|
+
from a deviation of 180 degrees, the deviation shrinks in magnitude when the
|
|
41
|
+
relative rate and the deviation have opposite signs (applying) and grows when
|
|
42
|
+
they have the same sign (separating). Statuses compare exact floats, with no
|
|
43
|
+
tolerance. `STATIONARY` means the relative rate is exactly zero, not that either
|
|
44
|
+
planet has stopped changing longitude (a station). `INDETERMINATE` means a rate
|
|
45
|
+
is missing, or the deviation is 180 degrees with nonzero relative motion:
|
|
46
|
+
its magnitude has no two-sided derivative there.
|
|
47
|
+
|
|
48
|
+
相位此刻在接近还是远离精确角度。相对黄经速率是第二个对象的速率减去第一个的速率。
|
|
49
|
+
偏差不为 180 度时,相对速率与偏差异号,偏差的绝对值在缩小(入相);同号时在增大(离相)。
|
|
50
|
+
直接比较浮点数,不使用容差。`STATIONARY` 指相对速率恰为零,
|
|
51
|
+
不表示某颗行星的黄经速率为零(占星术语中的“留”)。
|
|
52
|
+
`INDETERMINATE` 指缺少速率,或偏差恰为 180 度且存在相对运动,此时偏差的绝对值没有双侧导数。
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
APPLYING = "applying"
|
|
56
|
+
SEPARATING = "separating"
|
|
57
|
+
EXACT = "exact"
|
|
58
|
+
STATIONARY = "stationary"
|
|
59
|
+
INDETERMINATE = "indeterminate"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True, slots=True)
|
|
63
|
+
class AspectObject:
|
|
64
|
+
"""One object's longitude in degrees and its rate in degrees per day, if it has one.
|
|
65
|
+
|
|
66
|
+
The rate may be None where no meaningful motion exists; it is never borrowed from
|
|
67
|
+
another object. All objects must use the same day, such as the TT day.
|
|
68
|
+
|
|
69
|
+
一个对象的黄经(度)及其速率(度/日,可为 None)。若无法给出有意义的速率,就用 None,
|
|
70
|
+
不借用其他对象的速率。所有对象的速率必须使用相同的日长定义,例如 TT 日。
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
identity: Body | LunarNode
|
|
74
|
+
longitude_deg: float
|
|
75
|
+
longitude_rate_deg_per_day: float | None
|
|
76
|
+
|
|
77
|
+
def __post_init__(self) -> None:
|
|
78
|
+
if not isinstance(self.identity, (Body, LunarNode)):
|
|
79
|
+
raise TypeError("identity must be a Body or LunarNode")
|
|
80
|
+
object.__setattr__(self, "longitude_deg", finite_number(self.longitude_deg, "longitude_deg"))
|
|
81
|
+
if self.longitude_rate_deg_per_day is not None:
|
|
82
|
+
object.__setattr__(
|
|
83
|
+
self,
|
|
84
|
+
"longitude_rate_deg_per_day",
|
|
85
|
+
finite_number(self.longitude_rate_deg_per_day, "longitude_rate_deg_per_day"),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True, slots=True)
|
|
90
|
+
class AspectHit:
|
|
91
|
+
"""One aspect between two objects; the order of the pair and the branch are kept.
|
|
92
|
+
|
|
93
|
+
`branch_deg` is the exact oriented angle from the first object to the second that
|
|
94
|
+
the separation was measured against: minus or plus the aspect angle for a sextile,
|
|
95
|
+
square or trine, and the single angle 0 or 180 for a conjunction or opposition.
|
|
96
|
+
`deviation_deg` is the separation minus the branch, in (-180, 180]. Swapping the
|
|
97
|
+
pair flips the deviation, except that 180 is its own mirror, and flips the branch
|
|
98
|
+
where there are two; the status does not change.
|
|
99
|
+
|
|
100
|
+
两个对象之间符合规则的一条相位结果,保留对象顺序和分支。`branch_deg` 是用来对照间距的
|
|
101
|
+
精确角度,方向从第一个对象指向第二个:六分、四分、三分相取相位角的负值或正值,合相和对分相只有
|
|
102
|
+
0 或 180 这一个角度。`deviation_deg` 是间距减去分支,取值在 (-180, 180]。交换配对时,
|
|
103
|
+
偏差反号,但 180 仍记为 180;有两个分支时,分支也反号。运动状态不变。
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
first: Body | LunarNode
|
|
107
|
+
second: Body | LunarNode
|
|
108
|
+
aspect: MajorAspect
|
|
109
|
+
branch_deg: float
|
|
110
|
+
deviation_deg: float
|
|
111
|
+
orb_limit_deg: float
|
|
112
|
+
motion: AspectMotion
|
|
113
|
+
|
|
114
|
+
def __post_init__(self) -> None:
|
|
115
|
+
if not isinstance(self.first, (Body, LunarNode)) or not isinstance(
|
|
116
|
+
self.second, (Body, LunarNode)
|
|
117
|
+
):
|
|
118
|
+
raise TypeError("Aspect endpoints must be Body or LunarNode values")
|
|
119
|
+
if self.first == self.second or (
|
|
120
|
+
isinstance(self.first, LunarNode) and isinstance(self.second, LunarNode)
|
|
121
|
+
):
|
|
122
|
+
raise ValueError("An aspect needs distinct endpoints, not two lunar nodes")
|
|
123
|
+
if type(self.aspect) is not MajorAspect or type(self.motion) is not AspectMotion:
|
|
124
|
+
raise TypeError("aspect and motion must use their owning enum types")
|
|
125
|
+
for name in ("branch_deg", "deviation_deg", "orb_limit_deg"):
|
|
126
|
+
object.__setattr__(self, name, finite_number(getattr(self, name), name))
|
|
127
|
+
if self.branch_deg not in _branches(aspect_angle(self.aspect)):
|
|
128
|
+
raise ValueError("branch_deg does not belong to the aspect")
|
|
129
|
+
if not -180.0 < self.deviation_deg <= 180.0:
|
|
130
|
+
raise ValueError("deviation_deg must be in (-180, 180]")
|
|
131
|
+
if self.orb_limit_deg < 0.0 or abs(self.deviation_deg) > self.orb_limit_deg:
|
|
132
|
+
raise ValueError("The aspect deviation must be within its nonnegative orb limit")
|
|
133
|
+
if (self.deviation_deg == 0.0) != (self.motion is AspectMotion.EXACT):
|
|
134
|
+
raise ValueError("Only a zero deviation has exact motion status")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _signed(angle: float) -> float:
|
|
138
|
+
"""Wrap an angle to (-180, 180], preserving small values near zero.
|
|
139
|
+
|
|
140
|
+
将角度折回 (-180, 180],保留接近零的小角度。
|
|
141
|
+
"""
|
|
142
|
+
value = remainder(angle, 360.0)
|
|
143
|
+
return 180.0 if value == -180.0 else value
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _deviation(separation: float, branch: float) -> float:
|
|
147
|
+
"""Separation minus branch in (-180, 180], with 180 as its own mirror.
|
|
148
|
+
|
|
149
|
+
For the branch 180, `remainder` of a sum near -360 rounds to zero while the mirrored
|
|
150
|
+
pair stays a few ulps away, which would make the two orders disagree on the status.
|
|
151
|
+
Adding or subtracting 180 by the sign of the separation keeps both exact near zero.
|
|
152
|
+
|
|
153
|
+
间距减去分支,结果折回 (-180, 180];交换对象后,180 仍记为正值。
|
|
154
|
+
分支为 180 时,直接相减再用 `remainder` 可能把接近 -360 的结果舍入成零,
|
|
155
|
+
交换对象后却仍差几个 ulp,导致两种顺序的运动状态不同。
|
|
156
|
+
按间距的正负选择加或减 180,可以保留两种顺序下接近零的偏差。
|
|
157
|
+
"""
|
|
158
|
+
if branch == 180.0:
|
|
159
|
+
return _signed(separation - 180.0 if separation > 0.0 else separation + 180.0)
|
|
160
|
+
return _signed(separation - branch)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def aspect_angle(aspect: MajorAspect) -> float:
|
|
164
|
+
"""Return the exact angle of an aspect in degrees: 0, 60, 90, 120 or 180.
|
|
165
|
+
|
|
166
|
+
返回相位的精确角度(度):0、60、90、120 或 180。
|
|
167
|
+
"""
|
|
168
|
+
if not isinstance(aspect, MajorAspect):
|
|
169
|
+
raise TypeError("aspect must be a MajorAspect")
|
|
170
|
+
return _ANGLES[aspect]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _branches(angle: float) -> tuple[float, ...]:
|
|
174
|
+
if angle in (0.0, 180.0):
|
|
175
|
+
return (angle,)
|
|
176
|
+
return (-angle, angle)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _mean(first: float, second: float) -> float:
|
|
180
|
+
"""Return the correctly rounded mean of two finite nonnegative numbers, never infinite.
|
|
181
|
+
|
|
182
|
+
`(first + second) / 2` rounds once, even into the subnormal range, as long as the sum
|
|
183
|
+
does not overflow. When either number is above half the largest float the sum might
|
|
184
|
+
overflow, so both are halved first; a number that large has a rounding step so coarse
|
|
185
|
+
that halving a tiny partner cannot change the result.
|
|
186
|
+
|
|
187
|
+
返回两个有限非负数正确舍入后的平均值,结果不会是无穷。只要和不溢出,`(first + second) / 2`
|
|
188
|
+
只舍入一次,次正规数范围内也是如此。任一数大于最大浮点数的一半时,和可能溢出,就先各自除以
|
|
189
|
+
2;这么大的数,相邻浮点值之间的间隔也很大,另一个很小的数除以 2 不会改变结果。
|
|
190
|
+
"""
|
|
191
|
+
if first > _HALF_MAXIMUM or second > _HALF_MAXIMUM:
|
|
192
|
+
return first / 2.0 + second / 2.0
|
|
193
|
+
return (first + second) / 2.0
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _limit(
|
|
197
|
+
rule: AspectRule,
|
|
198
|
+
first: Body | LunarNode,
|
|
199
|
+
second: Body | LunarNode,
|
|
200
|
+
combination: OrbCombination | None,
|
|
201
|
+
) -> float:
|
|
202
|
+
own = {entry.identity: entry.orb_degrees for entry in rule.object_orbs}
|
|
203
|
+
first_orb = own.get(first, rule.orb_degrees)
|
|
204
|
+
second_orb = own.get(second, rule.orb_degrees)
|
|
205
|
+
if combination is OrbCombination.BOTH_WITHIN_OWN_ORB:
|
|
206
|
+
return min(first_orb, second_orb)
|
|
207
|
+
if combination is OrbCombination.MEAN_OF_ORBS:
|
|
208
|
+
return _mean(first_orb, second_orb)
|
|
209
|
+
return max(first_orb, second_orb)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _motion(deviation: float, first: AspectObject, second: AspectObject) -> AspectMotion:
|
|
213
|
+
if deviation == 0.0:
|
|
214
|
+
return AspectMotion.EXACT
|
|
215
|
+
if first.longitude_rate_deg_per_day is None or second.longitude_rate_deg_per_day is None:
|
|
216
|
+
return AspectMotion.INDETERMINATE
|
|
217
|
+
relative = second.longitude_rate_deg_per_day - first.longitude_rate_deg_per_day
|
|
218
|
+
if relative == 0.0:
|
|
219
|
+
return AspectMotion.STATIONARY
|
|
220
|
+
if deviation == 180.0:
|
|
221
|
+
return AspectMotion.INDETERMINATE
|
|
222
|
+
return AspectMotion.SEPARATING if (deviation > 0.0) == (relative > 0.0) else AspectMotion.APPLYING
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def find_aspects(
|
|
226
|
+
objects: Sequence[AspectObject],
|
|
227
|
+
rules: Sequence[AspectRule],
|
|
228
|
+
combination: OrbCombination | None = None,
|
|
229
|
+
) -> tuple[AspectHit, ...]:
|
|
230
|
+
"""Return every aspect that holds between pairs of objects, in a fixed order.
|
|
231
|
+
|
|
232
|
+
Pairs follow the order of `objects`, then the aspect angle, then the branch (minus
|
|
233
|
+
before plus), so the order is total. Every matching aspect and branch is returned:
|
|
234
|
+
broad orbs may give one pair several hits, and none is dropped in favor of a nearer
|
|
235
|
+
one. A hit holds when the deviation's magnitude is at most the limit, with the limit
|
|
236
|
+
itself included. The limit is the rule's orb, or the combination of the two objects'
|
|
237
|
+
orbs when the rule has per-object orbs, which then requires `combination`. Two lunar
|
|
238
|
+
nodes never form an aspect: the four identities are one pair of nodes under two
|
|
239
|
+
definitions and two directions.
|
|
240
|
+
|
|
241
|
+
按固定顺序返回对象两两之间符合规则的所有相位。配对按 `objects` 的顺序排列,再按相位角排序,
|
|
242
|
+
同一相位的负分支排在正分支之前。容许度较宽时,同一对对象可能符合多个相位或分支,都会返回,
|
|
243
|
+
不会只保留偏差最小的一个。偏差的绝对值不超过容许度上限就算符合,等于上限也算。
|
|
244
|
+
上限通常是规则的容许度;规则给对象单独设置容许度时,上限由两个对象的容许度合成,
|
|
245
|
+
此时必须给出 `combination`。两个月交点之间不构成相位:四个标识表示同一对交点的两种定义和两个方向。
|
|
246
|
+
"""
|
|
247
|
+
if not isinstance(objects, Sequence) or isinstance(objects, (str, bytes)):
|
|
248
|
+
raise TypeError("objects must be a sequence of AspectObject values")
|
|
249
|
+
members = tuple(objects)
|
|
250
|
+
if any(type(member) is not AspectObject for member in members):
|
|
251
|
+
raise TypeError("objects must contain only AspectObject values")
|
|
252
|
+
if len({member.identity for member in members}) != len(members):
|
|
253
|
+
raise ValueError("objects must not contain duplicate identities")
|
|
254
|
+
if not isinstance(rules, Sequence) or isinstance(rules, (str, bytes)):
|
|
255
|
+
raise TypeError("rules must be a sequence of AspectRule values")
|
|
256
|
+
ordered = tuple(rules)
|
|
257
|
+
if any(type(rule) is not AspectRule for rule in ordered):
|
|
258
|
+
raise TypeError("rules must contain only AspectRule values")
|
|
259
|
+
if len({rule.aspect for rule in ordered}) != len(ordered):
|
|
260
|
+
raise ValueError("rules must not repeat an aspect identity")
|
|
261
|
+
if combination is not None and not isinstance(combination, OrbCombination):
|
|
262
|
+
raise TypeError("combination must be an OrbCombination")
|
|
263
|
+
if combination is None and any(rule.object_orbs for rule in ordered):
|
|
264
|
+
raise ValueError("combination is required when rules have object orbs")
|
|
265
|
+
|
|
266
|
+
by_angle = sorted(ordered, key=lambda rule: _ANGLES[rule.aspect])
|
|
267
|
+
hits: list[AspectHit] = []
|
|
268
|
+
for index, first in enumerate(members):
|
|
269
|
+
for second in members[index + 1 :]:
|
|
270
|
+
if isinstance(first.identity, LunarNode) and isinstance(second.identity, LunarNode):
|
|
271
|
+
continue
|
|
272
|
+
separation = _signed(_signed(second.longitude_deg) - _signed(first.longitude_deg))
|
|
273
|
+
for rule in by_angle:
|
|
274
|
+
limit = _limit(rule, first.identity, second.identity, combination)
|
|
275
|
+
for branch in _branches(_ANGLES[rule.aspect]):
|
|
276
|
+
deviation = _deviation(separation, branch)
|
|
277
|
+
if abs(deviation) <= limit:
|
|
278
|
+
hits.append(
|
|
279
|
+
AspectHit(
|
|
280
|
+
first.identity,
|
|
281
|
+
second.identity,
|
|
282
|
+
rule.aspect,
|
|
283
|
+
branch,
|
|
284
|
+
deviation,
|
|
285
|
+
limit,
|
|
286
|
+
_motion(deviation, first, second),
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
return tuple(hits)
|