Skip to content

Commit 7ded17e

Browse files
committed
[core] added horizontal and vertical resection methods
1 parent 02d1efa commit 7ded17e

File tree

3 files changed

+328
-1
lines changed

3 files changed

+328
-1
lines changed

.github/workflows/run-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
strategy:
1414
fail-fast: false
1515
matrix:
16-
python-version: ["3.11", "3.12", "3.13", "3.14"]
16+
python-version: ["3.11", "3.12", "3.13"]
1717

1818
steps:
1919
- uses: actions/checkout@v4

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ dependencies = [
2020
"jmespath ~= 1.0.1",
2121
"toml ~= 0.10.2",
2222
"PyYAML ~= 6.0.2",
23+
"numpy ~= 2.0",
2324
"cloup ~= 3.0.7",
2425
"click-extra ~= 5.0.2"
2526
]

src/instrumentman/calculations.py

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import math
22
from typing import Sequence
33

4+
import numpy as np
45
from geocompy.data import Angle, Coordinate
56

67

@@ -57,3 +58,328 @@ def preliminary_resection(
5758
Angle(180, "deg") - vs1,
5859
ds1
5960
)
61+
62+
63+
def _matrices_resection_horizontal(
64+
measurements: list[tuple[Angle, Angle, float]],
65+
targets: list[Coordinate],
66+
station: Coordinate,
67+
orientation: Angle
68+
) -> tuple[list[list[float]], list[float]]:
69+
design_matrix: list[list[float]] = []
70+
observation_vector: list[float] = []
71+
72+
for (hz, v, d), coord in zip(measurements, targets):
73+
dr0 = coord - station
74+
dx0, dy0, dz0 = dr0
75+
hz0, v0, dsd0 = dr0.to_polar()
76+
dhd0 = dr0.to_2d().length()
77+
78+
hz_o = (hz + orientation).normalized()
79+
design_matrix.append(
80+
[
81+
-1,
82+
-dx0 / dhd0**2,
83+
dy0 / dhd0**2
84+
]
85+
)
86+
design_matrix.append(
87+
[
88+
0,
89+
-dx0 / dhd0,
90+
-dy0 / dhd0
91+
]
92+
)
93+
observation_vector.extend(
94+
[
95+
float(hz0.relative_to(hz_o)),
96+
dhd0 - math.sin(v) * d
97+
]
98+
)
99+
100+
return design_matrix, observation_vector
101+
102+
103+
def _matrices_resection_vertical(
104+
measurements: list[tuple[Angle, Angle, float]],
105+
targets: list[Coordinate],
106+
station: Coordinate
107+
) -> tuple[list[list[float]], list[float]]:
108+
design_matrix: list[list[float]] = []
109+
observation_vector: list[float] = []
110+
111+
for (_, v, d), coord in zip(measurements, targets):
112+
dr0 = coord - station
113+
114+
design_matrix.append(
115+
[
116+
-1
117+
]
118+
)
119+
observation_vector.extend(
120+
[
121+
dr0.z - d * math.cos(v)
122+
]
123+
)
124+
125+
return design_matrix, observation_vector
126+
127+
128+
def _weights_resection_horizontal(
129+
measurements: list[tuple[Angle, Angle, float]],
130+
targets: list[Coordinate],
131+
*,
132+
accuracy_hz: float = 1,
133+
accuracy_v: float = 1,
134+
accuracy_d: tuple[float, float] = (1, 1.5)
135+
) -> np.ndarray:
136+
weights: list[float] = []
137+
138+
accuracy_sd, accuracy_sd_ppm = accuracy_d
139+
for _, v, sd in measurements:
140+
weights.extend(
141+
(
142+
1 / accuracy_hz**2,
143+
1 / (
144+
(
145+
(
146+
accuracy_sd
147+
+ accuracy_sd_ppm * sd / 1000
148+
) * math.sin(v)
149+
)**2
150+
+ (
151+
sd * math.cos(v) * accuracy_v
152+
)**2
153+
)
154+
)
155+
)
156+
157+
return np.diag(weights)
158+
159+
160+
def _weights_resection_vertical(
161+
measurements: list[tuple[Angle, Angle, float]],
162+
targets: list[Coordinate],
163+
*,
164+
accuracy_v: float = 1
165+
) -> np.ndarray:
166+
weights: list[float] = []
167+
168+
accuracy_v_rad = math.radians(accuracy_v / 3600)
169+
for _, v, sd in measurements:
170+
hd = math.sin(v) * sd
171+
if hd < 30:
172+
hd = 30
173+
weights.append(
174+
1 / (
175+
(hd * 5e-5)**2
176+
+ (
177+
hd * accuracy_v_rad
178+
)**2
179+
)
180+
)
181+
182+
return np.diag(weights)
183+
184+
185+
def _iter_resection_horizontal(
186+
measurements: list[tuple[Angle, Angle, float]],
187+
targets: list[Coordinate],
188+
station: Coordinate,
189+
orientation: Angle,
190+
weights: np.ndarray,
191+
) -> tuple[np.ndarray, np.ndarray]:
192+
design_float, obs_float = _matrices_resection_horizontal(
193+
measurements,
194+
targets,
195+
station,
196+
orientation
197+
)
198+
199+
design = np.array(design_float)
200+
obs = np.array(obs_float)
201+
202+
norm = design.T @ weights @ design
203+
norminv = np.linalg.pinv(norm)
204+
205+
x = -norminv @ design.T @ weights @ obs
206+
207+
v = design @ x - obs
208+
m0 = np.sqrt(v.T @ weights @ v / (len(measurements) * 2 - 3))
209+
m = m0 * np.sqrt(np.diag(norminv))
210+
211+
return x, m
212+
213+
214+
def _iter_resection_vertical(
215+
measurements: list[tuple[Angle, Angle, float]],
216+
targets: list[Coordinate],
217+
station: Coordinate,
218+
weights: np.ndarray
219+
) -> tuple[np.ndarray, np.ndarray]:
220+
design_float, obs_float = _matrices_resection_vertical(
221+
measurements,
222+
targets,
223+
station
224+
)
225+
226+
design = np.array(design_float)
227+
obs = np.array(obs_float)
228+
229+
norm = design.T @ weights @ design
230+
norminv = np.linalg.pinv(norm)
231+
232+
x = -norminv @ design.T @ weights @ obs
233+
234+
v = design @ x - obs
235+
m0 = np.sqrt(v.T @ weights @ v / (len(measurements) - 1))
236+
m = m0 * np.sqrt(np.diag(norminv))
237+
238+
return x, m
239+
240+
241+
def resection_horizontal(
242+
measurements: list[tuple[Angle, Angle, float]],
243+
targets: list[Coordinate],
244+
preliminary_station: Coordinate,
245+
*,
246+
x_tolerance: float = 5e-4,
247+
y_tolerance: float = 5e-4,
248+
orientation_tolerance: Angle = Angle(1 / 3600, "deg"),
249+
max_iterations: int = 20,
250+
uniform_weights: bool = False,
251+
accuracy_hz: float = 1,
252+
accuracy_v: float = 1,
253+
accuracy_d: tuple[float, float] = (1, 1.5)
254+
) -> tuple[bool, Angle, Angle, Coordinate, Coordinate]:
255+
if len(measurements) != len(targets) or len(targets) < 2:
256+
raise ValueError("Cannot calculate resection with less than 2 targets")
257+
258+
station = preliminary_station
259+
delta_st0, _, _ = (targets[0] - station).to_polar()
260+
orientation = (delta_st0 - measurements[0][0]).normalized()
261+
262+
o_tolerance = float(orientation_tolerance)
263+
264+
if uniform_weights:
265+
weights = np.eye(len(measurements) * 2)
266+
else:
267+
weights = _weights_resection_horizontal(
268+
measurements,
269+
targets,
270+
accuracy_hz=accuracy_hz,
271+
accuracy_v=accuracy_v,
272+
accuracy_d=accuracy_d
273+
)
274+
275+
stdev_orientation = Angle(0)
276+
stdev_station = Coordinate(0, 0, 0)
277+
corr_o = corr_x = corr_y = np.inf
278+
iterations = 0
279+
while (
280+
abs(corr_o) > o_tolerance
281+
or abs(corr_x) > x_tolerance
282+
or abs(corr_y) > y_tolerance
283+
):
284+
if iterations >= max_iterations:
285+
return (
286+
False, orientation, stdev_orientation, station, stdev_station
287+
)
288+
289+
x, m = _iter_resection_horizontal(
290+
measurements,
291+
targets,
292+
station,
293+
orientation,
294+
weights
295+
)
296+
297+
corr_o, corr_x, corr_y = x
298+
299+
orientation = (orientation + Angle(corr_o)).normalized()
300+
station = station + Coordinate(corr_x, corr_y, 0)
301+
# seemingly should be deg, but not sure
302+
stdev_orientation = Angle(m[0])
303+
stdev_station = Coordinate(m[1], m[2], 0)
304+
305+
iterations += 1
306+
307+
return True, orientation, stdev_orientation, station, stdev_station
308+
309+
310+
def resection_vertical(
311+
measurements: list[tuple[Angle, Angle, float]],
312+
targets: list[Coordinate],
313+
preliminary_station: Coordinate,
314+
*,
315+
uniform_weights: bool = False,
316+
accuracy_v: float = 1
317+
) -> tuple[Coordinate, Coordinate]:
318+
if uniform_weights:
319+
weights = np.eye(len(measurements))
320+
else:
321+
weights = _weights_resection_vertical(
322+
measurements,
323+
targets,
324+
accuracy_v=accuracy_v
325+
)
326+
327+
x, m = _iter_resection_vertical(
328+
measurements,
329+
targets,
330+
preliminary_station,
331+
weights
332+
)
333+
334+
return preliminary_station + Coordinate(0, 0, x[0]), Coordinate(0, 0, m[0])
335+
336+
337+
def resection_2d_1d(
338+
measurements: list[tuple[Angle, Angle, float]],
339+
targets: list[Coordinate],
340+
preliminary_station: Coordinate,
341+
*,
342+
x_tolerance: float = 5e-4,
343+
y_tolerance: float = 5e-4,
344+
orientation_tolerance: Angle = Angle(1 / 3600, "deg"),
345+
max_iterations: int = 20,
346+
uniform_weights: bool = False,
347+
accuracy_hz: float = 1,
348+
accuracy_v: float = 1,
349+
accuracy_d: tuple[float, float] = (1, 1.5)
350+
) -> tuple[bool, Angle, Angle, Coordinate, Coordinate]:
351+
(
352+
converged,
353+
orientation,
354+
stdev_orientation,
355+
station,
356+
stdev_station_hz
357+
) = resection_horizontal(
358+
measurements,
359+
targets,
360+
preliminary_station,
361+
x_tolerance=x_tolerance,
362+
y_tolerance=y_tolerance,
363+
orientation_tolerance=orientation_tolerance,
364+
max_iterations=max_iterations,
365+
accuracy_hz=accuracy_hz,
366+
accuracy_v=accuracy_v,
367+
accuracy_d=accuracy_d,
368+
uniform_weights=uniform_weights
369+
)
370+
371+
station, stdev_station_v = resection_vertical(
372+
measurements,
373+
targets,
374+
station,
375+
uniform_weights=uniform_weights,
376+
accuracy_v=accuracy_v
377+
)
378+
379+
return (
380+
converged,
381+
orientation,
382+
stdev_orientation,
383+
station,
384+
stdev_station_hz + stdev_station_v
385+
)

0 commit comments

Comments
 (0)