ballistic-solve 0.0.1__cp313-cp313-win_amd64.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.
- ballistic-solve/__init__.py +64 -0
- ballistic-solve/_core.pyi +553 -0
- ballistic_solve/_core.cp313-win_amd64.pyd +0 -0
- ballistic_solve/_core.pyi +553 -0
- ballistic_solve-0.0.1.dist-info/METADATA +58 -0
- ballistic_solve-0.0.1.dist-info/RECORD +8 -0
- ballistic_solve-0.0.1.dist-info/WHEEL +5 -0
- ballistic_solve-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Ballistic trajectory simulation and optimization library"""
|
|
2
|
+
|
|
3
|
+
from ._core import (
|
|
4
|
+
# Core classes
|
|
5
|
+
Environment,
|
|
6
|
+
Projectile,
|
|
7
|
+
Platform,
|
|
8
|
+
Trajectory,
|
|
9
|
+
IntegrationOptions,
|
|
10
|
+
TargetingSolution,
|
|
11
|
+
TargetingOptions,
|
|
12
|
+
AngleSearchStrategy,
|
|
13
|
+
|
|
14
|
+
# Utility functions
|
|
15
|
+
to_direction,
|
|
16
|
+
to_angles,
|
|
17
|
+
|
|
18
|
+
# Trajectory computation
|
|
19
|
+
compute_trajectory,
|
|
20
|
+
compute_point,
|
|
21
|
+
|
|
22
|
+
# Targeting functions
|
|
23
|
+
find_best_azimuth_angle,
|
|
24
|
+
find_best_elevation_angle,
|
|
25
|
+
find_best_angles,
|
|
26
|
+
find_best_direction,
|
|
27
|
+
find_earliest_time,
|
|
28
|
+
find_latest_time,
|
|
29
|
+
find_earliest_firing_solution,
|
|
30
|
+
find_latest_firing_solution,
|
|
31
|
+
|
|
32
|
+
# Optimization tools
|
|
33
|
+
bracket_find_root,
|
|
34
|
+
basin_find_minima,
|
|
35
|
+
sinlike_find_minima,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__version__ = "0.0.1"
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"Environment",
|
|
42
|
+
"Projectile",
|
|
43
|
+
"Platform",
|
|
44
|
+
"Trajectory",
|
|
45
|
+
"IntegrationOptions",
|
|
46
|
+
"TargetingSolution",
|
|
47
|
+
"TargetingOptions",
|
|
48
|
+
"AngleSearchStrategy",
|
|
49
|
+
"to_direction",
|
|
50
|
+
"to_angles",
|
|
51
|
+
"compute_trajectory",
|
|
52
|
+
"compute_point",
|
|
53
|
+
"find_best_azimuth_angle",
|
|
54
|
+
"find_best_elevation_angle",
|
|
55
|
+
"find_best_angles",
|
|
56
|
+
"find_best_direction",
|
|
57
|
+
"find_earliest_time",
|
|
58
|
+
"find_latest_time",
|
|
59
|
+
"find_earliest_firing_solution",
|
|
60
|
+
"find_latest_firing_solution",
|
|
61
|
+
"bracket_find_root",
|
|
62
|
+
"basin_find_minima",
|
|
63
|
+
"sinlike_find_minima",
|
|
64
|
+
]
|
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
from collections.abc import Callable, Sequence
|
|
2
|
+
import enum
|
|
3
|
+
from typing import Annotated, overload
|
|
4
|
+
|
|
5
|
+
import numpy
|
|
6
|
+
from numpy.typing import NDArray
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Environment:
|
|
10
|
+
"""
|
|
11
|
+
Environmental conditions for ballistic simulation.
|
|
12
|
+
|
|
13
|
+
Encapsulates atmospheric and gravitational conditions affecting projectile motion.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, gravity: Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')], air_density: Callable[[float], float], wind_velocity: Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]) -> None:
|
|
17
|
+
"""Wind velocity vector (m/s)"""
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def gravity(self) -> Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]:
|
|
21
|
+
"""Gravitational acceleration vector in m/s²"""
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def air_density(self) -> Callable[[float], float]:
|
|
25
|
+
"""Air density function ρ(altitude) in kg/m³"""
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def wind_velocity(self) -> Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]:
|
|
29
|
+
"""Wind velocity vector in m/s"""
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def vacuum() -> Environment:
|
|
33
|
+
"""
|
|
34
|
+
Create a vacuum environment (no air, no wind).
|
|
35
|
+
|
|
36
|
+
Uses standard gravity but zero air density. Useful for testing.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def standard() -> Environment:
|
|
41
|
+
"""
|
|
42
|
+
Create standard Earth atmosphere at sea level.
|
|
43
|
+
|
|
44
|
+
Uses standard gravity (9.81 m/s²) and exponential atmosphere model.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def standard_with_random_wind(seed: int | None = None) -> Environment:
|
|
49
|
+
"""Optional random seed for reproducibility"""
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def standard_with_random_strong_wind(seed: int | None = None) -> Environment:
|
|
53
|
+
"""Optional random seed for reproducibility"""
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def standard_with_random_extreme_wind(seed: int | None = None) -> Environment:
|
|
57
|
+
"""Optional random seed for reproducibility"""
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def standard_with_random_catastrophic_wind(seed: int | None = None) -> Environment:
|
|
61
|
+
"""Optional random seed for reproducibility"""
|
|
62
|
+
|
|
63
|
+
class Projectile:
|
|
64
|
+
"""
|
|
65
|
+
Physical properties of a projectile for ballistic simulation.
|
|
66
|
+
|
|
67
|
+
Encapsulates the mass, cross-sectional area, and drag coefficient
|
|
68
|
+
needed to compute aerodynamic forces during flight.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, mass: float = 1.0, area: float = 1.0, drag_coefficient: float = 0.47) -> None:
|
|
72
|
+
"""Dimensionless drag coefficient (default: 0.47, sphere)"""
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def mass(self) -> float:
|
|
76
|
+
"""Mass of the projectile in kilograms"""
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def area(self) -> float:
|
|
80
|
+
"""Cross-sectional area in square meters (perpendicular to velocity)"""
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def drag_coefficient(self) -> float:
|
|
84
|
+
"""
|
|
85
|
+
Dimensionless drag coefficient. Typical values:
|
|
86
|
+
- Sphere: ~0.47
|
|
87
|
+
- Streamlined body: ~0.04
|
|
88
|
+
- Flat plate: ~1.28
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
class Platform:
|
|
92
|
+
"""
|
|
93
|
+
Launch platform properties for ballistic simulation.
|
|
94
|
+
|
|
95
|
+
Represents the firing platform (e.g., ground position, moving vehicle, aircraft)
|
|
96
|
+
including its position, velocity, and the muzzle velocity of the weapon system.
|
|
97
|
+
The projectile's initial velocity is the vector sum of platform velocity and
|
|
98
|
+
muzzle velocity in the launch direction.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __init__(self, position: Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')], velocity: Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')], muzzle_velocity: float) -> None:
|
|
102
|
+
"""Scalar muzzle velocity in m/s (projectile speed relative to platform)"""
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def position(self) -> Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]:
|
|
106
|
+
"""Position of the launch platform in 3D space (meters)"""
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def velocity(self) -> Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]:
|
|
110
|
+
"""
|
|
111
|
+
Velocity of the platform itself (m/s). For stationary platforms, this is zero.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def muzzle_velocity(self) -> float:
|
|
116
|
+
"""
|
|
117
|
+
Muzzle velocity magnitude (m/s). Speed of projectile relative to platform.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
class Trajectory:
|
|
121
|
+
"""
|
|
122
|
+
Container for a computed ballistic trajectory.
|
|
123
|
+
|
|
124
|
+
Stores the position history and corresponding time stamps of a projectile
|
|
125
|
+
as computed by the trajectory integration. Positions and times are recorded
|
|
126
|
+
at each adaptive integration step.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(self, positions: Sequence[Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]], times: Sequence[float]) -> None:
|
|
130
|
+
"""Vector of time stamps (must have same length as positions)"""
|
|
131
|
+
|
|
132
|
+
def size(self) -> int:
|
|
133
|
+
"""Get the number of recorded points in the trajectory."""
|
|
134
|
+
|
|
135
|
+
def empty(self) -> bool:
|
|
136
|
+
"""Check if the trajectory is empty."""
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def positions(self) -> list[Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]]:
|
|
140
|
+
"""Sequence of 3D positions along the trajectory"""
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def times(self) -> list[float]:
|
|
144
|
+
"""Corresponding time stamps for each position (seconds)"""
|
|
145
|
+
|
|
146
|
+
class IntegrationOptions:
|
|
147
|
+
"""
|
|
148
|
+
Configuration for the adaptive ODE integrator.
|
|
149
|
+
|
|
150
|
+
Controls the Dormand-Prince (DOPRI5) Runge-Kutta adaptive stepper.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def __init__(self, max_step: float = 1000.0, first_step: float = 0.001, atol: float = 1e-06, rtol: float = 0.001) -> None:
|
|
154
|
+
"""Relative error tolerance"""
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def max_step(self) -> float:
|
|
158
|
+
"""Maximum step size (seconds)"""
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def first_step(self) -> float:
|
|
162
|
+
"""Initial step size (seconds)"""
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def atol(self) -> float:
|
|
166
|
+
"""Absolute error tolerance"""
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def rtol(self) -> float:
|
|
170
|
+
"""Relative error tolerance"""
|
|
171
|
+
|
|
172
|
+
class TargetingSolution:
|
|
173
|
+
"""
|
|
174
|
+
Complete targeting solution for engaging a target.
|
|
175
|
+
|
|
176
|
+
Contains launch angles, direction vector, and time of flight.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def angles(self) -> tuple[float, float]:
|
|
181
|
+
"""Launch angles as (azimuth, elevation) in radians"""
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def direction(self) -> Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]:
|
|
185
|
+
"""Launch direction (unit vector)"""
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def time(self) -> float:
|
|
189
|
+
"""Time of flight (seconds)"""
|
|
190
|
+
|
|
191
|
+
class AngleSearchStrategy(enum.Enum):
|
|
192
|
+
"""Strategy for nested angle optimization."""
|
|
193
|
+
|
|
194
|
+
AzimuthThenElevation = 0
|
|
195
|
+
"""Optimize azimuth in inner loop, elevation in outer loop"""
|
|
196
|
+
|
|
197
|
+
ElevationThenAzimuth = 1
|
|
198
|
+
"""Optimize elevation in inner loop, azimuth in outer loop"""
|
|
199
|
+
|
|
200
|
+
class TargetingOptions:
|
|
201
|
+
"""
|
|
202
|
+
Configuration for the ballistic solver.
|
|
203
|
+
|
|
204
|
+
Controls search strategy and iteration limits for finding firing solutions.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
def __init__(self, angle_search_strategy: AngleSearchStrategy = AngleSearchStrategy.AzimuthThenElevation, azimuth_max_iter: int = 16, elevation_max_iter: int = 16, time_max_iter: int = 16, h: float = 0.001, time_scan_step: float = 0.5) -> None:
|
|
208
|
+
"""Time step for coarse scanning in time search (seconds)"""
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def angle_search_strategy(self) -> AngleSearchStrategy:
|
|
212
|
+
"""Strategy for nested angle search"""
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def azimuth_max_iter(self) -> int:
|
|
216
|
+
"""Maximum iterations for azimuth angle search"""
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def elevation_max_iter(self) -> int:
|
|
220
|
+
"""Maximum iterations for elevation angle search"""
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def time_max_iter(self) -> int:
|
|
224
|
+
"""Maximum iterations for time-of-flight search"""
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def h(self) -> float:
|
|
228
|
+
"""Step size for numerical differentiation"""
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def time_scan_step(self) -> float:
|
|
232
|
+
"""Time step for coarse scanning in time search (seconds)"""
|
|
233
|
+
|
|
234
|
+
def to_direction(angles: tuple[float, float]) -> Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]:
|
|
235
|
+
"""
|
|
236
|
+
Convert spherical coordinates to 3D direction vector.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
angles: Pair of (azimuth, elevation) angles in radians
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Unit direction vector
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
def to_angles(direction: Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]) -> tuple[float, float]:
|
|
246
|
+
"""
|
|
247
|
+
Convert 3D direction vector to spherical coordinates.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
direction: Direction vector (need not be normalized)
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Tuple of (azimuth, elevation) in radians
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
@overload
|
|
257
|
+
def compute_trajectory(environment: Environment, projectile: Projectile, platform: Platform, direction: Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')], time: float, integration_options: IntegrationOptions = ...) -> Trajectory:
|
|
258
|
+
"""
|
|
259
|
+
Compute complete ballistic trajectory with recorded time steps.
|
|
260
|
+
|
|
261
|
+
Simulates projectile motion under gravity, drag, and wind using adaptive
|
|
262
|
+
Runge-Kutta integration. Records position at each integration step.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
environment: Environmental conditions
|
|
266
|
+
projectile: Projectile properties
|
|
267
|
+
platform: Launch platform
|
|
268
|
+
direction: Unit vector indicating launch direction
|
|
269
|
+
time: Simulation time (seconds)
|
|
270
|
+
integration_options: Integration parameters
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Complete trajectory with positions and times
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
@overload
|
|
277
|
+
def compute_trajectory(environment: Environment, projectile: Projectile, platform: Platform, angles: tuple[float, float], time: float, integration_options: IntegrationOptions = ...) -> Trajectory:
|
|
278
|
+
"""
|
|
279
|
+
Compute complete ballistic trajectory from launch angles.
|
|
280
|
+
|
|
281
|
+
Convenience overload that accepts (azimuth, elevation) angles.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
environment: Environmental conditions
|
|
285
|
+
projectile: Projectile properties
|
|
286
|
+
platform: Launch platform
|
|
287
|
+
angles: Pair of (azimuth, elevation) angles in radians
|
|
288
|
+
time: Simulation time (seconds)
|
|
289
|
+
integration_options: Integration parameters
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Complete trajectory with positions and times
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
@overload
|
|
296
|
+
def compute_point(environment: Environment, projectile: Projectile, platform: Platform, direction: Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')], time: float, integration_options: IntegrationOptions = ...) -> Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]:
|
|
297
|
+
"""
|
|
298
|
+
Compute final position only (no trajectory recording).
|
|
299
|
+
|
|
300
|
+
More efficient than compute_trajectory when only the endpoint is needed.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
environment: Environmental conditions
|
|
304
|
+
projectile: Projectile properties
|
|
305
|
+
platform: Launch platform
|
|
306
|
+
direction: Unit vector indicating launch direction
|
|
307
|
+
time: Simulation time (seconds)
|
|
308
|
+
integration_options: Integration parameters
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Final position after given simulation time
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
@overload
|
|
315
|
+
def compute_point(environment: Environment, projectile: Projectile, platform: Platform, angles: tuple[float, float], time: float, integration_options: IntegrationOptions = ...) -> Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]:
|
|
316
|
+
"""
|
|
317
|
+
Compute final position only from launch angles.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
environment: Environmental conditions
|
|
321
|
+
projectile: Projectile properties
|
|
322
|
+
platform: Launch platform
|
|
323
|
+
angles: Pair of (azimuth, elevation) angles in radians
|
|
324
|
+
time: Simulation time (seconds)
|
|
325
|
+
integration_options: Integration parameters
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Final position after given simulation time
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
def find_best_azimuth_angle(environment: Environment, projectile: Projectile, platform: Platform, target_position: Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')], elevation_angle: float, time: float, integration_options: IntegrationOptions = ..., targeting_options: TargetingOptions = ...) -> float:
|
|
332
|
+
"""
|
|
333
|
+
Find best azimuth angle for a given elevation and time.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
environment: Environmental conditions
|
|
337
|
+
projectile: Projectile properties
|
|
338
|
+
platform: Launch platform
|
|
339
|
+
target_position: Target position
|
|
340
|
+
elevation_angle: Fixed elevation angle (radians)
|
|
341
|
+
time: Flight time (seconds)
|
|
342
|
+
integration_options: Integration parameters
|
|
343
|
+
targeting_options: Targeting configuration
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
Best azimuth angle (radians)
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
def find_best_elevation_angle(environment: Environment, projectile: Projectile, platform: Platform, target_position: Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')], azimuth_angle: float, time: float, integration_options: IntegrationOptions = ..., targeting_options: TargetingOptions = ...) -> float:
|
|
350
|
+
"""
|
|
351
|
+
Find best elevation angle for a given azimuth and time.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
environment: Environmental conditions
|
|
355
|
+
projectile: Projectile properties
|
|
356
|
+
platform: Launch platform
|
|
357
|
+
target_position: Target position
|
|
358
|
+
azimuth_angle: Fixed azimuth angle (radians)
|
|
359
|
+
time: Flight time (seconds)
|
|
360
|
+
integration_options: Integration parameters
|
|
361
|
+
targeting_options: Targeting configuration
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
Best elevation angle (radians)
|
|
365
|
+
"""
|
|
366
|
+
|
|
367
|
+
def find_best_angles(environment: Environment, projectile: Projectile, platform: Platform, target_position: Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')], time: float, integration_options: IntegrationOptions = ..., targeting_options: TargetingOptions = ...) -> tuple[float, float]:
|
|
368
|
+
"""
|
|
369
|
+
Find best launch angles (azimuth, elevation) for a given time.
|
|
370
|
+
|
|
371
|
+
Uses nested search based on the configured angle search strategy.
|
|
372
|
+
Minimizes squared distance to target.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
environment: Environmental conditions
|
|
376
|
+
projectile: Projectile properties
|
|
377
|
+
platform: Launch platform
|
|
378
|
+
target_position: Target position
|
|
379
|
+
time: Flight time (seconds)
|
|
380
|
+
integration_options: Integration parameters
|
|
381
|
+
targeting_options: Targeting configuration
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
Tuple of (azimuth, elevation) in radians
|
|
385
|
+
"""
|
|
386
|
+
|
|
387
|
+
def find_best_direction(environment: Environment, projectile: Projectile, platform: Platform, target_position: Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')], time: float, integration_options: IntegrationOptions = ..., targeting_options: TargetingOptions = ...) -> Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]:
|
|
388
|
+
"""
|
|
389
|
+
Find best launch direction for a given time.
|
|
390
|
+
|
|
391
|
+
Convenience function that returns a direction vector instead of angles.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
environment: Environmental conditions
|
|
395
|
+
projectile: Projectile properties
|
|
396
|
+
platform: Launch platform
|
|
397
|
+
target_position: Target position
|
|
398
|
+
time: Flight time (seconds)
|
|
399
|
+
integration_options: Integration parameters
|
|
400
|
+
targeting_options: Targeting configuration
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
Best launch direction (unit vector)
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
def find_earliest_time(environment: Environment, projectile: Projectile, platform: Platform, target_position: Callable[[float], Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]], time_range: tuple[float, float], integration_options: IntegrationOptions = ..., targeting_options: TargetingOptions = ...) -> float | None:
|
|
407
|
+
"""
|
|
408
|
+
Find earliest time when target can be intercepted.
|
|
409
|
+
|
|
410
|
+
Searches forward through the time range to find the first moment when
|
|
411
|
+
the projectile can reach the (possibly moving) target. Uses coarse
|
|
412
|
+
scanning followed by root refinement.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
environment: Environmental conditions
|
|
416
|
+
projectile: Projectile properties
|
|
417
|
+
platform: Launch platform
|
|
418
|
+
target_position: Function returning target position at given time
|
|
419
|
+
time_range: Tuple of (min_time, max_time) in seconds
|
|
420
|
+
integration_options: Integration parameters
|
|
421
|
+
targeting_options: Targeting configuration
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
Earliest intercept time in seconds, or None if no solution exists
|
|
425
|
+
"""
|
|
426
|
+
|
|
427
|
+
def find_latest_time(environment: Environment, projectile: Projectile, platform: Platform, target_position: Callable[[float], Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]], time_range: tuple[float, float], integration_options: IntegrationOptions = ..., targeting_options: TargetingOptions = ...) -> float | None:
|
|
428
|
+
"""
|
|
429
|
+
Find latest time when target can be intercepted.
|
|
430
|
+
|
|
431
|
+
Searches backward through the time range to find the last moment when
|
|
432
|
+
the projectile can reach the (possibly moving) target. Uses coarse
|
|
433
|
+
scanning followed by root refinement.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
environment: Environmental conditions
|
|
437
|
+
projectile: Projectile properties
|
|
438
|
+
platform: Launch platform
|
|
439
|
+
target_position: Function returning target position at given time
|
|
440
|
+
time_range: Tuple of (min_time, max_time) in seconds
|
|
441
|
+
integration_options: Integration parameters
|
|
442
|
+
targeting_options: Targeting configuration
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
Latest intercept time in seconds, or None if no solution exists
|
|
446
|
+
"""
|
|
447
|
+
|
|
448
|
+
def find_earliest_firing_solution(environment: Environment, projectile: Projectile, platform: Platform, target_position: Callable[[float], Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]], time_range: tuple[float, float], integration_options: IntegrationOptions = ..., targeting_options: TargetingOptions = ...) -> TargetingSolution | None:
|
|
449
|
+
"""
|
|
450
|
+
Find earliest firing solution for intercepting a target.
|
|
451
|
+
|
|
452
|
+
Combines find_earliest_time with find_best_angles to produce a complete
|
|
453
|
+
targeting solution at the earliest possible intercept time.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
environment: Environmental conditions
|
|
457
|
+
projectile: Projectile properties
|
|
458
|
+
platform: Launch platform
|
|
459
|
+
target_position: Function returning target position at given time
|
|
460
|
+
time_range: Tuple of (min_time, max_time) in seconds
|
|
461
|
+
integration_options: Integration parameters
|
|
462
|
+
targeting_options: Targeting configuration
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
Complete firing solution, or None if no solution exists
|
|
466
|
+
"""
|
|
467
|
+
|
|
468
|
+
def find_latest_firing_solution(environment: Environment, projectile: Projectile, platform: Platform, target_position: Callable[[float], Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]], time_range: tuple[float, float], integration_options: IntegrationOptions = ..., targeting_options: TargetingOptions = ...) -> TargetingSolution | None:
|
|
469
|
+
"""
|
|
470
|
+
Find latest firing solution for intercepting a target.
|
|
471
|
+
|
|
472
|
+
Combines find_latest_time with find_best_angles to produce a complete
|
|
473
|
+
targeting solution at the latest possible intercept time.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
environment: Environmental conditions
|
|
477
|
+
projectile: Projectile properties
|
|
478
|
+
platform: Launch platform
|
|
479
|
+
target_position: Function returning target position at given time
|
|
480
|
+
time_range: Tuple of (min_time, max_time) in seconds
|
|
481
|
+
integration_options: Integration parameters
|
|
482
|
+
targeting_options: Targeting configuration
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
Complete firing solution, or None if no solution exists
|
|
486
|
+
"""
|
|
487
|
+
|
|
488
|
+
def bracket_find_root(f: Callable[[float], float], a_x: float, b_x: float, max_iter: int = 16) -> tuple[float, float]:
|
|
489
|
+
"""
|
|
490
|
+
Find a bracketing interval containing a root using TOMS748 algorithm.
|
|
491
|
+
|
|
492
|
+
Returns an interval [a, b] where f(a) and f(b) have opposite signs,
|
|
493
|
+
guaranteeing a root exists within the interval by the intermediate value theorem.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
f: Objective function to find root of
|
|
497
|
+
a_x: Lower bound of search interval
|
|
498
|
+
b_x: Upper bound of search interval
|
|
499
|
+
max_iter: Maximum number of iterations (default: 32)
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
Bracket (a, b) containing the root
|
|
503
|
+
|
|
504
|
+
Note:
|
|
505
|
+
f(a_x) and f(b_x) must have opposite signs
|
|
506
|
+
"""
|
|
507
|
+
|
|
508
|
+
def basin_find_minima(f: Callable[[float], float], lo_x: float, hi_x: float, max_iter: int = 16) -> tuple[float, float]:
|
|
509
|
+
"""
|
|
510
|
+
Find minimum of a unimodal (single basin) function using Brent's method.
|
|
511
|
+
|
|
512
|
+
Efficiently finds the global minimum for convex or unimodal functions.
|
|
513
|
+
Uses parabolic interpolation with golden section search as fallback.
|
|
514
|
+
|
|
515
|
+
Args:
|
|
516
|
+
f: Objective function to minimize
|
|
517
|
+
lo_x: Lower bound of search interval
|
|
518
|
+
hi_x: Upper bound of search interval
|
|
519
|
+
max_iter: Maximum number of iterations (default: 32)
|
|
520
|
+
|
|
521
|
+
Returns:
|
|
522
|
+
Tuple of (x_min, f(x_min)) - location and value of minimum
|
|
523
|
+
"""
|
|
524
|
+
|
|
525
|
+
def sinlike_find_minima(f: Callable[[float], float], lo_x: float, hi_x: float, h: float = 0.001, max_iter: int = 16) -> tuple[float, float]:
|
|
526
|
+
"""
|
|
527
|
+
Find global minimum of a sine-like periodic function with one complete wave.
|
|
528
|
+
|
|
529
|
+
Specialized algorithm for functions with approximately sinusoidal behavior
|
|
530
|
+
containing a single global minimum per period. Uses boundary sampling to
|
|
531
|
+
detect topology, then partitions the domain via root-finding and applies
|
|
532
|
+
Brent's method to each subregion.
|
|
533
|
+
|
|
534
|
+
Algorithm strategy:
|
|
535
|
+
1. Sample near boundaries to detect if minimum is trivial (at boundary)
|
|
536
|
+
2. If function is monotonic, search entire interval
|
|
537
|
+
3. If multimodal, find level curve crossing and partition domain
|
|
538
|
+
4. Apply basin optimization to each partition independently
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
f: Objective function to minimize (should be sine-like)
|
|
542
|
+
lo_x: Lower bound of search interval
|
|
543
|
+
hi_x: Upper bound of search interval
|
|
544
|
+
h: Step size for boundary probing (default: 1e-3)
|
|
545
|
+
max_iter: Maximum iterations per optimization call (default: 32)
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
Tuple of (x_min, f(x_min)) - global minimum
|
|
549
|
+
|
|
550
|
+
Note:
|
|
551
|
+
- Assumes function has at most one complete oscillation in [lo_x, hi_x]
|
|
552
|
+
- Requires hi_x - lo_x > 2*h for proper boundary sampling
|
|
553
|
+
"""
|
|
Binary file
|
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
from collections.abc import Callable, Sequence
|
|
2
|
+
import enum
|
|
3
|
+
from typing import Annotated, overload
|
|
4
|
+
|
|
5
|
+
import numpy
|
|
6
|
+
from numpy.typing import NDArray
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Environment:
|
|
10
|
+
"""
|
|
11
|
+
Environmental conditions for ballistic simulation.
|
|
12
|
+
|
|
13
|
+
Encapsulates atmospheric and gravitational conditions affecting projectile motion.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, gravity: Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')], air_density: Callable[[float], float], wind_velocity: Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]) -> None:
|
|
17
|
+
"""Wind velocity vector (m/s)"""
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def gravity(self) -> Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]:
|
|
21
|
+
"""Gravitational acceleration vector in m/s²"""
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def air_density(self) -> Callable[[float], float]:
|
|
25
|
+
"""Air density function ρ(altitude) in kg/m³"""
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def wind_velocity(self) -> Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]:
|
|
29
|
+
"""Wind velocity vector in m/s"""
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def vacuum() -> Environment:
|
|
33
|
+
"""
|
|
34
|
+
Create a vacuum environment (no air, no wind).
|
|
35
|
+
|
|
36
|
+
Uses standard gravity but zero air density. Useful for testing.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def standard() -> Environment:
|
|
41
|
+
"""
|
|
42
|
+
Create standard Earth atmosphere at sea level.
|
|
43
|
+
|
|
44
|
+
Uses standard gravity (9.81 m/s²) and exponential atmosphere model.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def standard_with_random_wind(seed: int | None = None) -> Environment:
|
|
49
|
+
"""Optional random seed for reproducibility"""
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def standard_with_random_strong_wind(seed: int | None = None) -> Environment:
|
|
53
|
+
"""Optional random seed for reproducibility"""
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def standard_with_random_extreme_wind(seed: int | None = None) -> Environment:
|
|
57
|
+
"""Optional random seed for reproducibility"""
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def standard_with_random_catastrophic_wind(seed: int | None = None) -> Environment:
|
|
61
|
+
"""Optional random seed for reproducibility"""
|
|
62
|
+
|
|
63
|
+
class Projectile:
|
|
64
|
+
"""
|
|
65
|
+
Physical properties of a projectile for ballistic simulation.
|
|
66
|
+
|
|
67
|
+
Encapsulates the mass, cross-sectional area, and drag coefficient
|
|
68
|
+
needed to compute aerodynamic forces during flight.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, mass: float = 1.0, area: float = 1.0, drag_coefficient: float = 0.47) -> None:
|
|
72
|
+
"""Dimensionless drag coefficient (default: 0.47, sphere)"""
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def mass(self) -> float:
|
|
76
|
+
"""Mass of the projectile in kilograms"""
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def area(self) -> float:
|
|
80
|
+
"""Cross-sectional area in square meters (perpendicular to velocity)"""
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def drag_coefficient(self) -> float:
|
|
84
|
+
"""
|
|
85
|
+
Dimensionless drag coefficient. Typical values:
|
|
86
|
+
- Sphere: ~0.47
|
|
87
|
+
- Streamlined body: ~0.04
|
|
88
|
+
- Flat plate: ~1.28
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
class Platform:
|
|
92
|
+
"""
|
|
93
|
+
Launch platform properties for ballistic simulation.
|
|
94
|
+
|
|
95
|
+
Represents the firing platform (e.g., ground position, moving vehicle, aircraft)
|
|
96
|
+
including its position, velocity, and the muzzle velocity of the weapon system.
|
|
97
|
+
The projectile's initial velocity is the vector sum of platform velocity and
|
|
98
|
+
muzzle velocity in the launch direction.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __init__(self, position: Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')], velocity: Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')], muzzle_velocity: float) -> None:
|
|
102
|
+
"""Scalar muzzle velocity in m/s (projectile speed relative to platform)"""
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def position(self) -> Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]:
|
|
106
|
+
"""Position of the launch platform in 3D space (meters)"""
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def velocity(self) -> Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]:
|
|
110
|
+
"""
|
|
111
|
+
Velocity of the platform itself (m/s). For stationary platforms, this is zero.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def muzzle_velocity(self) -> float:
|
|
116
|
+
"""
|
|
117
|
+
Muzzle velocity magnitude (m/s). Speed of projectile relative to platform.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
class Trajectory:
|
|
121
|
+
"""
|
|
122
|
+
Container for a computed ballistic trajectory.
|
|
123
|
+
|
|
124
|
+
Stores the position history and corresponding time stamps of a projectile
|
|
125
|
+
as computed by the trajectory integration. Positions and times are recorded
|
|
126
|
+
at each adaptive integration step.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(self, positions: Sequence[Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]], times: Sequence[float]) -> None:
|
|
130
|
+
"""Vector of time stamps (must have same length as positions)"""
|
|
131
|
+
|
|
132
|
+
def size(self) -> int:
|
|
133
|
+
"""Get the number of recorded points in the trajectory."""
|
|
134
|
+
|
|
135
|
+
def empty(self) -> bool:
|
|
136
|
+
"""Check if the trajectory is empty."""
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def positions(self) -> list[Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]]:
|
|
140
|
+
"""Sequence of 3D positions along the trajectory"""
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def times(self) -> list[float]:
|
|
144
|
+
"""Corresponding time stamps for each position (seconds)"""
|
|
145
|
+
|
|
146
|
+
class IntegrationOptions:
|
|
147
|
+
"""
|
|
148
|
+
Configuration for the adaptive ODE integrator.
|
|
149
|
+
|
|
150
|
+
Controls the Dormand-Prince (DOPRI5) Runge-Kutta adaptive stepper.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def __init__(self, max_step: float = 1000.0, first_step: float = 0.001, atol: float = 1e-06, rtol: float = 0.001) -> None:
|
|
154
|
+
"""Relative error tolerance"""
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def max_step(self) -> float:
|
|
158
|
+
"""Maximum step size (seconds)"""
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def first_step(self) -> float:
|
|
162
|
+
"""Initial step size (seconds)"""
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def atol(self) -> float:
|
|
166
|
+
"""Absolute error tolerance"""
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def rtol(self) -> float:
|
|
170
|
+
"""Relative error tolerance"""
|
|
171
|
+
|
|
172
|
+
class TargetingSolution:
|
|
173
|
+
"""
|
|
174
|
+
Complete targeting solution for engaging a target.
|
|
175
|
+
|
|
176
|
+
Contains launch angles, direction vector, and time of flight.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def angles(self) -> tuple[float, float]:
|
|
181
|
+
"""Launch angles as (azimuth, elevation) in radians"""
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def direction(self) -> Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]:
|
|
185
|
+
"""Launch direction (unit vector)"""
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def time(self) -> float:
|
|
189
|
+
"""Time of flight (seconds)"""
|
|
190
|
+
|
|
191
|
+
class AngleSearchStrategy(enum.Enum):
|
|
192
|
+
"""Strategy for nested angle optimization."""
|
|
193
|
+
|
|
194
|
+
AzimuthThenElevation = 0
|
|
195
|
+
"""Optimize azimuth in inner loop, elevation in outer loop"""
|
|
196
|
+
|
|
197
|
+
ElevationThenAzimuth = 1
|
|
198
|
+
"""Optimize elevation in inner loop, azimuth in outer loop"""
|
|
199
|
+
|
|
200
|
+
class TargetingOptions:
|
|
201
|
+
"""
|
|
202
|
+
Configuration for the ballistic solver.
|
|
203
|
+
|
|
204
|
+
Controls search strategy and iteration limits for finding firing solutions.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
def __init__(self, angle_search_strategy: AngleSearchStrategy = AngleSearchStrategy.AzimuthThenElevation, azimuth_max_iter: int = 16, elevation_max_iter: int = 16, time_max_iter: int = 16, h: float = 0.001, time_scan_step: float = 0.5) -> None:
|
|
208
|
+
"""Time step for coarse scanning in time search (seconds)"""
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def angle_search_strategy(self) -> AngleSearchStrategy:
|
|
212
|
+
"""Strategy for nested angle search"""
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def azimuth_max_iter(self) -> int:
|
|
216
|
+
"""Maximum iterations for azimuth angle search"""
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def elevation_max_iter(self) -> int:
|
|
220
|
+
"""Maximum iterations for elevation angle search"""
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def time_max_iter(self) -> int:
|
|
224
|
+
"""Maximum iterations for time-of-flight search"""
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def h(self) -> float:
|
|
228
|
+
"""Step size for numerical differentiation"""
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def time_scan_step(self) -> float:
|
|
232
|
+
"""Time step for coarse scanning in time search (seconds)"""
|
|
233
|
+
|
|
234
|
+
def to_direction(angles: tuple[float, float]) -> Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]:
|
|
235
|
+
"""
|
|
236
|
+
Convert spherical coordinates to 3D direction vector.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
angles: Pair of (azimuth, elevation) angles in radians
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Unit direction vector
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
def to_angles(direction: Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]) -> tuple[float, float]:
|
|
246
|
+
"""
|
|
247
|
+
Convert 3D direction vector to spherical coordinates.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
direction: Direction vector (need not be normalized)
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Tuple of (azimuth, elevation) in radians
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
@overload
|
|
257
|
+
def compute_trajectory(environment: Environment, projectile: Projectile, platform: Platform, direction: Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')], time: float, integration_options: IntegrationOptions = ...) -> Trajectory:
|
|
258
|
+
"""
|
|
259
|
+
Compute complete ballistic trajectory with recorded time steps.
|
|
260
|
+
|
|
261
|
+
Simulates projectile motion under gravity, drag, and wind using adaptive
|
|
262
|
+
Runge-Kutta integration. Records position at each integration step.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
environment: Environmental conditions
|
|
266
|
+
projectile: Projectile properties
|
|
267
|
+
platform: Launch platform
|
|
268
|
+
direction: Unit vector indicating launch direction
|
|
269
|
+
time: Simulation time (seconds)
|
|
270
|
+
integration_options: Integration parameters
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Complete trajectory with positions and times
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
@overload
|
|
277
|
+
def compute_trajectory(environment: Environment, projectile: Projectile, platform: Platform, angles: tuple[float, float], time: float, integration_options: IntegrationOptions = ...) -> Trajectory:
|
|
278
|
+
"""
|
|
279
|
+
Compute complete ballistic trajectory from launch angles.
|
|
280
|
+
|
|
281
|
+
Convenience overload that accepts (azimuth, elevation) angles.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
environment: Environmental conditions
|
|
285
|
+
projectile: Projectile properties
|
|
286
|
+
platform: Launch platform
|
|
287
|
+
angles: Pair of (azimuth, elevation) angles in radians
|
|
288
|
+
time: Simulation time (seconds)
|
|
289
|
+
integration_options: Integration parameters
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Complete trajectory with positions and times
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
@overload
|
|
296
|
+
def compute_point(environment: Environment, projectile: Projectile, platform: Platform, direction: Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')], time: float, integration_options: IntegrationOptions = ...) -> Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]:
|
|
297
|
+
"""
|
|
298
|
+
Compute final position only (no trajectory recording).
|
|
299
|
+
|
|
300
|
+
More efficient than compute_trajectory when only the endpoint is needed.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
environment: Environmental conditions
|
|
304
|
+
projectile: Projectile properties
|
|
305
|
+
platform: Launch platform
|
|
306
|
+
direction: Unit vector indicating launch direction
|
|
307
|
+
time: Simulation time (seconds)
|
|
308
|
+
integration_options: Integration parameters
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Final position after given simulation time
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
@overload
|
|
315
|
+
def compute_point(environment: Environment, projectile: Projectile, platform: Platform, angles: tuple[float, float], time: float, integration_options: IntegrationOptions = ...) -> Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]:
|
|
316
|
+
"""
|
|
317
|
+
Compute final position only from launch angles.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
environment: Environmental conditions
|
|
321
|
+
projectile: Projectile properties
|
|
322
|
+
platform: Launch platform
|
|
323
|
+
angles: Pair of (azimuth, elevation) angles in radians
|
|
324
|
+
time: Simulation time (seconds)
|
|
325
|
+
integration_options: Integration parameters
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Final position after given simulation time
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
def find_best_azimuth_angle(environment: Environment, projectile: Projectile, platform: Platform, target_position: Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')], elevation_angle: float, time: float, integration_options: IntegrationOptions = ..., targeting_options: TargetingOptions = ...) -> float:
|
|
332
|
+
"""
|
|
333
|
+
Find best azimuth angle for a given elevation and time.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
environment: Environmental conditions
|
|
337
|
+
projectile: Projectile properties
|
|
338
|
+
platform: Launch platform
|
|
339
|
+
target_position: Target position
|
|
340
|
+
elevation_angle: Fixed elevation angle (radians)
|
|
341
|
+
time: Flight time (seconds)
|
|
342
|
+
integration_options: Integration parameters
|
|
343
|
+
targeting_options: Targeting configuration
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
Best azimuth angle (radians)
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
def find_best_elevation_angle(environment: Environment, projectile: Projectile, platform: Platform, target_position: Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')], azimuth_angle: float, time: float, integration_options: IntegrationOptions = ..., targeting_options: TargetingOptions = ...) -> float:
|
|
350
|
+
"""
|
|
351
|
+
Find best elevation angle for a given azimuth and time.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
environment: Environmental conditions
|
|
355
|
+
projectile: Projectile properties
|
|
356
|
+
platform: Launch platform
|
|
357
|
+
target_position: Target position
|
|
358
|
+
azimuth_angle: Fixed azimuth angle (radians)
|
|
359
|
+
time: Flight time (seconds)
|
|
360
|
+
integration_options: Integration parameters
|
|
361
|
+
targeting_options: Targeting configuration
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
Best elevation angle (radians)
|
|
365
|
+
"""
|
|
366
|
+
|
|
367
|
+
def find_best_angles(environment: Environment, projectile: Projectile, platform: Platform, target_position: Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')], time: float, integration_options: IntegrationOptions = ..., targeting_options: TargetingOptions = ...) -> tuple[float, float]:
|
|
368
|
+
"""
|
|
369
|
+
Find best launch angles (azimuth, elevation) for a given time.
|
|
370
|
+
|
|
371
|
+
Uses nested search based on the configured angle search strategy.
|
|
372
|
+
Minimizes squared distance to target.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
environment: Environmental conditions
|
|
376
|
+
projectile: Projectile properties
|
|
377
|
+
platform: Launch platform
|
|
378
|
+
target_position: Target position
|
|
379
|
+
time: Flight time (seconds)
|
|
380
|
+
integration_options: Integration parameters
|
|
381
|
+
targeting_options: Targeting configuration
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
Tuple of (azimuth, elevation) in radians
|
|
385
|
+
"""
|
|
386
|
+
|
|
387
|
+
def find_best_direction(environment: Environment, projectile: Projectile, platform: Platform, target_position: Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')], time: float, integration_options: IntegrationOptions = ..., targeting_options: TargetingOptions = ...) -> Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]:
|
|
388
|
+
"""
|
|
389
|
+
Find best launch direction for a given time.
|
|
390
|
+
|
|
391
|
+
Convenience function that returns a direction vector instead of angles.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
environment: Environmental conditions
|
|
395
|
+
projectile: Projectile properties
|
|
396
|
+
platform: Launch platform
|
|
397
|
+
target_position: Target position
|
|
398
|
+
time: Flight time (seconds)
|
|
399
|
+
integration_options: Integration parameters
|
|
400
|
+
targeting_options: Targeting configuration
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
Best launch direction (unit vector)
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
def find_earliest_time(environment: Environment, projectile: Projectile, platform: Platform, target_position: Callable[[float], Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]], time_range: tuple[float, float], integration_options: IntegrationOptions = ..., targeting_options: TargetingOptions = ...) -> float | None:
|
|
407
|
+
"""
|
|
408
|
+
Find earliest time when target can be intercepted.
|
|
409
|
+
|
|
410
|
+
Searches forward through the time range to find the first moment when
|
|
411
|
+
the projectile can reach the (possibly moving) target. Uses coarse
|
|
412
|
+
scanning followed by root refinement.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
environment: Environmental conditions
|
|
416
|
+
projectile: Projectile properties
|
|
417
|
+
platform: Launch platform
|
|
418
|
+
target_position: Function returning target position at given time
|
|
419
|
+
time_range: Tuple of (min_time, max_time) in seconds
|
|
420
|
+
integration_options: Integration parameters
|
|
421
|
+
targeting_options: Targeting configuration
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
Earliest intercept time in seconds, or None if no solution exists
|
|
425
|
+
"""
|
|
426
|
+
|
|
427
|
+
def find_latest_time(environment: Environment, projectile: Projectile, platform: Platform, target_position: Callable[[float], Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]], time_range: tuple[float, float], integration_options: IntegrationOptions = ..., targeting_options: TargetingOptions = ...) -> float | None:
|
|
428
|
+
"""
|
|
429
|
+
Find latest time when target can be intercepted.
|
|
430
|
+
|
|
431
|
+
Searches backward through the time range to find the last moment when
|
|
432
|
+
the projectile can reach the (possibly moving) target. Uses coarse
|
|
433
|
+
scanning followed by root refinement.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
environment: Environmental conditions
|
|
437
|
+
projectile: Projectile properties
|
|
438
|
+
platform: Launch platform
|
|
439
|
+
target_position: Function returning target position at given time
|
|
440
|
+
time_range: Tuple of (min_time, max_time) in seconds
|
|
441
|
+
integration_options: Integration parameters
|
|
442
|
+
targeting_options: Targeting configuration
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
Latest intercept time in seconds, or None if no solution exists
|
|
446
|
+
"""
|
|
447
|
+
|
|
448
|
+
def find_earliest_firing_solution(environment: Environment, projectile: Projectile, platform: Platform, target_position: Callable[[float], Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]], time_range: tuple[float, float], integration_options: IntegrationOptions = ..., targeting_options: TargetingOptions = ...) -> TargetingSolution | None:
|
|
449
|
+
"""
|
|
450
|
+
Find earliest firing solution for intercepting a target.
|
|
451
|
+
|
|
452
|
+
Combines find_earliest_time with find_best_angles to produce a complete
|
|
453
|
+
targeting solution at the earliest possible intercept time.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
environment: Environmental conditions
|
|
457
|
+
projectile: Projectile properties
|
|
458
|
+
platform: Launch platform
|
|
459
|
+
target_position: Function returning target position at given time
|
|
460
|
+
time_range: Tuple of (min_time, max_time) in seconds
|
|
461
|
+
integration_options: Integration parameters
|
|
462
|
+
targeting_options: Targeting configuration
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
Complete firing solution, or None if no solution exists
|
|
466
|
+
"""
|
|
467
|
+
|
|
468
|
+
def find_latest_firing_solution(environment: Environment, projectile: Projectile, platform: Platform, target_position: Callable[[float], Annotated[NDArray[numpy.float64], dict(shape=(3), order='C')]], time_range: tuple[float, float], integration_options: IntegrationOptions = ..., targeting_options: TargetingOptions = ...) -> TargetingSolution | None:
|
|
469
|
+
"""
|
|
470
|
+
Find latest firing solution for intercepting a target.
|
|
471
|
+
|
|
472
|
+
Combines find_latest_time with find_best_angles to produce a complete
|
|
473
|
+
targeting solution at the latest possible intercept time.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
environment: Environmental conditions
|
|
477
|
+
projectile: Projectile properties
|
|
478
|
+
platform: Launch platform
|
|
479
|
+
target_position: Function returning target position at given time
|
|
480
|
+
time_range: Tuple of (min_time, max_time) in seconds
|
|
481
|
+
integration_options: Integration parameters
|
|
482
|
+
targeting_options: Targeting configuration
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
Complete firing solution, or None if no solution exists
|
|
486
|
+
"""
|
|
487
|
+
|
|
488
|
+
def bracket_find_root(f: Callable[[float], float], a_x: float, b_x: float, max_iter: int = 16) -> tuple[float, float]:
|
|
489
|
+
"""
|
|
490
|
+
Find a bracketing interval containing a root using TOMS748 algorithm.
|
|
491
|
+
|
|
492
|
+
Returns an interval [a, b] where f(a) and f(b) have opposite signs,
|
|
493
|
+
guaranteeing a root exists within the interval by the intermediate value theorem.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
f: Objective function to find root of
|
|
497
|
+
a_x: Lower bound of search interval
|
|
498
|
+
b_x: Upper bound of search interval
|
|
499
|
+
max_iter: Maximum number of iterations (default: 32)
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
Bracket (a, b) containing the root
|
|
503
|
+
|
|
504
|
+
Note:
|
|
505
|
+
f(a_x) and f(b_x) must have opposite signs
|
|
506
|
+
"""
|
|
507
|
+
|
|
508
|
+
def basin_find_minima(f: Callable[[float], float], lo_x: float, hi_x: float, max_iter: int = 16) -> tuple[float, float]:
|
|
509
|
+
"""
|
|
510
|
+
Find minimum of a unimodal (single basin) function using Brent's method.
|
|
511
|
+
|
|
512
|
+
Efficiently finds the global minimum for convex or unimodal functions.
|
|
513
|
+
Uses parabolic interpolation with golden section search as fallback.
|
|
514
|
+
|
|
515
|
+
Args:
|
|
516
|
+
f: Objective function to minimize
|
|
517
|
+
lo_x: Lower bound of search interval
|
|
518
|
+
hi_x: Upper bound of search interval
|
|
519
|
+
max_iter: Maximum number of iterations (default: 32)
|
|
520
|
+
|
|
521
|
+
Returns:
|
|
522
|
+
Tuple of (x_min, f(x_min)) - location and value of minimum
|
|
523
|
+
"""
|
|
524
|
+
|
|
525
|
+
def sinlike_find_minima(f: Callable[[float], float], lo_x: float, hi_x: float, h: float = 0.001, max_iter: int = 16) -> tuple[float, float]:
|
|
526
|
+
"""
|
|
527
|
+
Find global minimum of a sine-like periodic function with one complete wave.
|
|
528
|
+
|
|
529
|
+
Specialized algorithm for functions with approximately sinusoidal behavior
|
|
530
|
+
containing a single global minimum per period. Uses boundary sampling to
|
|
531
|
+
detect topology, then partitions the domain via root-finding and applies
|
|
532
|
+
Brent's method to each subregion.
|
|
533
|
+
|
|
534
|
+
Algorithm strategy:
|
|
535
|
+
1. Sample near boundaries to detect if minimum is trivial (at boundary)
|
|
536
|
+
2. If function is monotonic, search entire interval
|
|
537
|
+
3. If multimodal, find level curve crossing and partition domain
|
|
538
|
+
4. Apply basin optimization to each partition independently
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
f: Objective function to minimize (should be sine-like)
|
|
542
|
+
lo_x: Lower bound of search interval
|
|
543
|
+
hi_x: Upper bound of search interval
|
|
544
|
+
h: Step size for boundary probing (default: 1e-3)
|
|
545
|
+
max_iter: Maximum iterations per optimization call (default: 32)
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
Tuple of (x_min, f(x_min)) - global minimum
|
|
549
|
+
|
|
550
|
+
Note:
|
|
551
|
+
- Assumes function has at most one complete oscillation in [lo_x, hi_x]
|
|
552
|
+
- Requires hi_x - lo_x > 2*h for proper boundary sampling
|
|
553
|
+
"""
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: ballistic-solve
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Ballistic trajectory solver with C++ core and Python bindings
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2026 Ariyudo Pertama
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Requires-Python: >=3.12
|
|
28
|
+
Requires-Dist: numpy
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# ballistic-solve
|
|
32
|
+
|
|
33
|
+
Ballistic trajectory simulation and fire control solutions.
|
|
34
|
+
|
|
35
|
+
## Development Setup
|
|
36
|
+
|
|
37
|
+
### Prerequisites
|
|
38
|
+
- Miniforge (or Conda/Miniconda)
|
|
39
|
+
|
|
40
|
+
### Installation
|
|
41
|
+
|
|
42
|
+
1. **Create the conda environment:**
|
|
43
|
+
```bat
|
|
44
|
+
conda env create --file environment.yml
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
2. **Activate the environment:**
|
|
48
|
+
```bat
|
|
49
|
+
conda activate ballistic-solve
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Development
|
|
53
|
+
|
|
54
|
+
Work in progress. More documentation coming soon.
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
ballistic-solve/__init__.py,sha256=h1XGQa-nEBsmIHF1_bWWB7__Guoo3_eJkQFg_CD_vGc,1426
|
|
2
|
+
ballistic-solve/_core.pyi,sha256=wtREmBU6HgRDGewTmfnTI_BFDembQVCka6_blSOANyM,22085
|
|
3
|
+
ballistic_solve/_core.cp313-win_amd64.pyd,sha256=z_QV8t9E5WcR92Ygl9hTdbiUZkDKxAHtRwjNnDlSy2s,243712
|
|
4
|
+
ballistic_solve/_core.pyi,sha256=wtREmBU6HgRDGewTmfnTI_BFDembQVCka6_blSOANyM,22085
|
|
5
|
+
ballistic_solve-0.0.1.dist-info/METADATA,sha256=UEGy6mkRW5WtSw_UAxtBZK96q1owJc-VBM7FHuiGlOQ,1913
|
|
6
|
+
ballistic_solve-0.0.1.dist-info/WHEEL,sha256=vkL3wTIkhjZa3RmEXX20hldNp6Q8qtwRjrXW6K5sw_Q,106
|
|
7
|
+
ballistic_solve-0.0.1.dist-info/licenses/LICENSE,sha256=9ITG7ET3y4ih4wwQJ5P1ljLes4skigI3vT9VLESrFWk,1072
|
|
8
|
+
ballistic_solve-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ariyudo Pertama
|
|
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.
|