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.
@@ -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
+ """
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: scikit-build-core 0.11.6
3
+ Root-Is-Purelib: false
4
+ Tag: cp313-cp313-win_amd64
5
+
@@ -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.