tiny-bclibc-wasm 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tiny_bclibc/__init__.py +765 -0
- tiny_bclibc/__init__.pyi +282 -0
- tiny_bclibc/_drag_tables.py +345 -0
- tiny_bclibc/_runner.py +446 -0
- tiny_bclibc/py.typed +0 -0
- tiny_bclibc/tiny_bclibc_dp.wasm +0 -0
- tiny_bclibc/tiny_bclibc_sp.wasm +0 -0
- tiny_bclibc_wasm-0.0.1.dist-info/METADATA +130 -0
- tiny_bclibc_wasm-0.0.1.dist-info/RECORD +11 -0
- tiny_bclibc_wasm-0.0.1.dist-info/WHEEL +5 -0
- tiny_bclibc_wasm-0.0.1.dist-info/top_level.txt +1 -0
tiny_bclibc/__init__.py
ADDED
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
"""tiny_bclibc -- micropython-bclibc's tiny_bclibc API for CPython, PyPy and Pythonista.
|
|
2
|
+
|
|
3
|
+
The same module a MicroPython board gets as a native .mpy (micropython-bclibc's natmod/usermod),
|
|
4
|
+
here backed by tiny_bclibc compiled to WebAssembly and run in whatever WebAssembly host is
|
|
5
|
+
available: JavaScriptCore's JSContext in Pythonista, `wasmtime` or Node on a desktop (see
|
|
6
|
+
`_runner.py`). A script written against the natmod runs here unchanged:
|
|
7
|
+
|
|
8
|
+
import tiny_bclibc as bc
|
|
9
|
+
|
|
10
|
+
shot = bc.Shot(bc=0.310, weight_grain=168.0, diameter_inch=0.308, length_inch=1.2,
|
|
11
|
+
muzzle_velocity_fps=2750.0, sight_height_ft=0.125, twist_inch=11.0)
|
|
12
|
+
bc.zero(shot, 300 * 3.28084)
|
|
13
|
+
rows, reason = bc.fire(shot, bc.Request(range_limit_ft=3000.0, range_step_ft=300.0))
|
|
14
|
+
for r in rows:
|
|
15
|
+
print(r[bc.T_DISTANCE], r[bc.T_HEIGHT])
|
|
16
|
+
|
|
17
|
+
Differences from the natmod, all at the storage level (results use the same C code):
|
|
18
|
+
- Shot/Wind/Config/Request keep their fields as Python floats, not float32 bytearrays, so a
|
|
19
|
+
double-precision module sees full-precision inputs. They are still `(buf, s)`-shaped
|
|
20
|
+
named tuples with the fields on `.s` (`shot.s.props.barrel_elevation_rad`, `w.s.velocity_fps`,
|
|
21
|
+
`cfg.s.max_iterations`, ...); `buf` is None.
|
|
22
|
+
- bench() measures the WebAssembly *host's* f32/f64 speed (the same loops, compiled to wasm),
|
|
23
|
+
not the CPU directly.
|
|
24
|
+
|
|
25
|
+
Typing: fully annotated (Python 3.10 syntax, which Pythonista runs); the public surface is also
|
|
26
|
+
described by __init__.pyi, checked against this module with mypy's stubtest.
|
|
27
|
+
|
|
28
|
+
Configuration (environment variables, read on first use):
|
|
29
|
+
TINY_BCLIBC_PRECISION double (default) | single -- which .wasm to load first; see
|
|
30
|
+
set_precision() to switch from code
|
|
31
|
+
TINY_BCLIBC_WASM explicit path to a .wasm (overrides the one next to this file)
|
|
32
|
+
TINY_BCLIBC_HOST jscontext | wasmtime | wasm3 | gi-jsc | node (default: first available,
|
|
33
|
+
see _runner.default_runner); set_host() does the same from code
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import atexit
|
|
37
|
+
import os
|
|
38
|
+
import struct as _struct
|
|
39
|
+
import time
|
|
40
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
41
|
+
from dataclasses import dataclass
|
|
42
|
+
from typing import Final, NamedTuple, TypeAlias
|
|
43
|
+
|
|
44
|
+
from . import _drag_tables
|
|
45
|
+
from ._runner import HOSTS, TbwError, WasmRunner, default_runner
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"DRAG_CUSTOM",
|
|
49
|
+
"DRAG_G1",
|
|
50
|
+
"DRAG_G7",
|
|
51
|
+
"INTERP_MACH",
|
|
52
|
+
"INTERP_POS_X",
|
|
53
|
+
"INTERP_POS_Y",
|
|
54
|
+
"INTERP_POS_Z",
|
|
55
|
+
"INTERP_TIME",
|
|
56
|
+
"INTERP_VEL_X",
|
|
57
|
+
"INTERP_VEL_Y",
|
|
58
|
+
"INTERP_VEL_Z",
|
|
59
|
+
"TRAJ_FLAG_ALL",
|
|
60
|
+
"TRAJ_FLAG_APEX",
|
|
61
|
+
"TRAJ_FLAG_MACH",
|
|
62
|
+
"TRAJ_FLAG_MRT",
|
|
63
|
+
"TRAJ_FLAG_NONE",
|
|
64
|
+
"TRAJ_FLAG_RANGE",
|
|
65
|
+
"TRAJ_FLAG_ZERO",
|
|
66
|
+
"TRAJ_FLAG_ZERO_DOWN",
|
|
67
|
+
"TRAJ_FLAG_ZERO_UP",
|
|
68
|
+
"T_ANGLE",
|
|
69
|
+
"T_DENSITY_RATIO",
|
|
70
|
+
"T_DISTANCE",
|
|
71
|
+
"T_DRAG",
|
|
72
|
+
"T_DROP_ANGLE",
|
|
73
|
+
"T_ENERGY",
|
|
74
|
+
"T_FLAG",
|
|
75
|
+
"T_HEIGHT",
|
|
76
|
+
"T_MACH",
|
|
77
|
+
"T_OGW",
|
|
78
|
+
"T_SLANT_DISTANCE",
|
|
79
|
+
"T_SLANT_HEIGHT",
|
|
80
|
+
"T_TIME",
|
|
81
|
+
"T_VELOCITY",
|
|
82
|
+
"T_WINDAGE",
|
|
83
|
+
"T_WINDAGE_ANGLE",
|
|
84
|
+
"Config",
|
|
85
|
+
"MultiBC",
|
|
86
|
+
"Request",
|
|
87
|
+
"Shot",
|
|
88
|
+
"Wind",
|
|
89
|
+
"aim",
|
|
90
|
+
"bench",
|
|
91
|
+
"build_multibc",
|
|
92
|
+
"find_apex",
|
|
93
|
+
"find_max_range",
|
|
94
|
+
"find_zero_angle",
|
|
95
|
+
"fire",
|
|
96
|
+
"host",
|
|
97
|
+
"integrate",
|
|
98
|
+
"integrate_at",
|
|
99
|
+
"integrate_ex",
|
|
100
|
+
"integrate_stream",
|
|
101
|
+
"precision",
|
|
102
|
+
"set_host",
|
|
103
|
+
"set_precision",
|
|
104
|
+
"version",
|
|
105
|
+
"zero",
|
|
106
|
+
"zero_point",
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
# ── Constants (same values as natmod's _tiny_bclibc) ──────────────────────────
|
|
110
|
+
DRAG_G1: Final = 0
|
|
111
|
+
DRAG_G7: Final = 1
|
|
112
|
+
DRAG_CUSTOM: Final = 2
|
|
113
|
+
|
|
114
|
+
TRAJ_FLAG_NONE: Final = 0
|
|
115
|
+
TRAJ_FLAG_ZERO_UP: Final = 1
|
|
116
|
+
TRAJ_FLAG_ZERO_DOWN: Final = 2
|
|
117
|
+
TRAJ_FLAG_ZERO: Final = 3
|
|
118
|
+
TRAJ_FLAG_MACH: Final = 4
|
|
119
|
+
TRAJ_FLAG_RANGE: Final = 8
|
|
120
|
+
TRAJ_FLAG_APEX: Final = 16
|
|
121
|
+
TRAJ_FLAG_MRT: Final = 32
|
|
122
|
+
TRAJ_FLAG_ALL: Final = 31
|
|
123
|
+
|
|
124
|
+
T_TIME: Final = 0
|
|
125
|
+
T_DISTANCE: Final = 1
|
|
126
|
+
T_VELOCITY: Final = 2
|
|
127
|
+
T_MACH: Final = 3
|
|
128
|
+
T_HEIGHT: Final = 4
|
|
129
|
+
T_SLANT_HEIGHT: Final = 5
|
|
130
|
+
T_DROP_ANGLE: Final = 6
|
|
131
|
+
T_WINDAGE: Final = 7
|
|
132
|
+
T_WINDAGE_ANGLE: Final = 8
|
|
133
|
+
T_SLANT_DISTANCE: Final = 9
|
|
134
|
+
T_ANGLE: Final = 10
|
|
135
|
+
T_DENSITY_RATIO: Final = 11
|
|
136
|
+
T_DRAG: Final = 12
|
|
137
|
+
T_ENERGY: Final = 13
|
|
138
|
+
T_OGW: Final = 14
|
|
139
|
+
T_FLAG: Final = 15
|
|
140
|
+
|
|
141
|
+
INTERP_TIME: Final = 0
|
|
142
|
+
INTERP_MACH: Final = 1
|
|
143
|
+
INTERP_POS_X: Final = 2
|
|
144
|
+
INTERP_POS_Y: Final = 3
|
|
145
|
+
INTERP_POS_Z: Final = 4
|
|
146
|
+
INTERP_VEL_X: Final = 5
|
|
147
|
+
INTERP_VEL_Y: Final = 6
|
|
148
|
+
INTERP_VEL_Z: Final = 7
|
|
149
|
+
|
|
150
|
+
_NaN: Final = float("nan")
|
|
151
|
+
_INF: Final = 1e8 # TINY_BCLIBC_MAX_WIND_DIST_FT
|
|
152
|
+
_MAX_WINDS: Final = 16
|
|
153
|
+
_MAX_DRAG_PTS: Final = 200
|
|
154
|
+
_TERM_HANDLER_STOP: Final = 5
|
|
155
|
+
|
|
156
|
+
# tiny_bclibc_wasm.c output layout
|
|
157
|
+
_ROW: Final = 16
|
|
158
|
+
_INTEGRATE_HEADER: Final = 12
|
|
159
|
+
_AT_HEADER: Final = 9
|
|
160
|
+
|
|
161
|
+
# ── Types ─────────────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
# One TrajectoryData row, indexed by the T_* constants: 15 floats, then the int TRAJ_FLAG_* flag.
|
|
164
|
+
Row: TypeAlias = tuple[
|
|
165
|
+
float, float, float, float, float, float, float, float, float, float, float, float, float, float, float, int
|
|
166
|
+
]
|
|
167
|
+
# BaseTrajData: (time, px, py, pz, vx, vy, vz, mach).
|
|
168
|
+
RawState: TypeAlias = tuple[float, float, float, float, float, float, float, float]
|
|
169
|
+
# A custom drag column: a packed float32 buffer (what MultiBC() returns) or a plain sequence.
|
|
170
|
+
DragColumn: TypeAlias = Sequence[float] | bytes | bytearray | memoryview
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ── Module loading ────────────────────────────────────────────────────────────
|
|
174
|
+
_HERE: Final = os.path.dirname(os.path.abspath(__file__))
|
|
175
|
+
_PRECISIONS: Final = ("double", "single")
|
|
176
|
+
# One loaded host per precision (named _actives so it can't shadow the _runner submodule).
|
|
177
|
+
_actives: dict[str, WasmRunner] = {}
|
|
178
|
+
_host_choice: str | WasmRunner | None = None
|
|
179
|
+
|
|
180
|
+
# Release the hosts while the interpreter is still intact: wasmtime's store/engine destructors
|
|
181
|
+
# otherwise run during shutdown, after their own module has been torn down, and print tracebacks.
|
|
182
|
+
atexit.register(_actives.clear)
|
|
183
|
+
_precision: str = "single" if os.environ.get("TINY_BCLIBC_PRECISION", "double").lower().startswith("s") else "double"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _get_runner() -> WasmRunner:
|
|
187
|
+
runner = _actives.get(_precision)
|
|
188
|
+
if runner is None:
|
|
189
|
+
path = os.environ.get("TINY_BCLIBC_WASM") or os.path.join(
|
|
190
|
+
_HERE, "tiny_bclibc_sp.wasm" if _precision == "single" else "tiny_bclibc_dp.wasm"
|
|
191
|
+
)
|
|
192
|
+
if not os.path.isfile(path):
|
|
193
|
+
raise FileNotFoundError(
|
|
194
|
+
f"tiny_bclibc wasm module not found at '{path}'. Build it with `uv sync` or "
|
|
195
|
+
"`python build_wasm.py` in the tiny-bclibc-wasm-py repo, or set "
|
|
196
|
+
"TINY_BCLIBC_WASM."
|
|
197
|
+
)
|
|
198
|
+
runner = _pick_host()
|
|
199
|
+
runner.load_file(path)
|
|
200
|
+
_actives[_precision] = runner
|
|
201
|
+
return runner
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _call(what: str, export: str, inputs: Sequence[float], *args: float) -> list[float]:
|
|
205
|
+
try:
|
|
206
|
+
return _get_runner().call(export, inputs, *args)
|
|
207
|
+
except TbwError as exc:
|
|
208
|
+
raise ValueError(f"{what} rc={exc.status}: {exc.message}") from None
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _pick_host() -> WasmRunner:
|
|
212
|
+
if _host_choice is None:
|
|
213
|
+
return default_runner()
|
|
214
|
+
if isinstance(_host_choice, WasmRunner):
|
|
215
|
+
return _host_choice
|
|
216
|
+
return HOSTS[_host_choice]()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def set_host(host: str | WasmRunner | None) -> None:
|
|
220
|
+
"""Choose the WebAssembly host before (or instead of) the automatic pick.
|
|
221
|
+
|
|
222
|
+
``host`` is a name (see _runner.HOSTS), a ready WasmRunner instance, or
|
|
223
|
+
None to go back to automatic selection. Takes effect on the next call (the module is reloaded).
|
|
224
|
+
"""
|
|
225
|
+
global _host_choice
|
|
226
|
+
if host is not None and not isinstance(host, WasmRunner) and host not in HOSTS:
|
|
227
|
+
raise ValueError("unknown host {!r}: expected one of {}".format(host, ", ".join(HOSTS)))
|
|
228
|
+
_host_choice = host
|
|
229
|
+
_actives.clear()
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def set_precision(precision: str) -> None:
|
|
233
|
+
"""Switch between the double- and single-precision modules ("double" | "single").
|
|
234
|
+
|
|
235
|
+
Process-wide, and cheap to flip back and forth: each module is loaded once, on first use.
|
|
236
|
+
"""
|
|
237
|
+
global _precision
|
|
238
|
+
if precision not in _PRECISIONS:
|
|
239
|
+
raise ValueError(f"unknown precision {precision!r}: expected one of {', '.join(_PRECISIONS)}")
|
|
240
|
+
_precision = precision
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def precision() -> str:
|
|
244
|
+
"""The precision in use: "double" or "single"."""
|
|
245
|
+
return _precision
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def version() -> str:
|
|
249
|
+
"""tiny_bclibc version of the loaded module, e.g. "2.0.0-rc.1-dp" (natmod: "<ver>-sp")."""
|
|
250
|
+
return _get_runner().version
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def host() -> str:
|
|
254
|
+
"""Name of the WebAssembly host in use: jscontext, wasmtime, wasm3, gi-jsc or node."""
|
|
255
|
+
return _get_runner().name
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ── Value objects ─────────────────────────────────────────────────────────────
|
|
259
|
+
# The `.s` side of natmod's `(buf, s)` pairs: there a uctypes struct view over a float32 buffer,
|
|
260
|
+
# here a plain typed record with the same field names.
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@dataclass(slots=True)
|
|
264
|
+
class WindFields:
|
|
265
|
+
velocity_fps: float
|
|
266
|
+
direction_from_rad: float
|
|
267
|
+
until_distance_ft: float
|
|
268
|
+
max_distance_ft: float
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@dataclass(slots=True)
|
|
272
|
+
class ConfigFields:
|
|
273
|
+
step_multiplier: float
|
|
274
|
+
zero_finding_accuracy: float
|
|
275
|
+
minimum_velocity: float
|
|
276
|
+
maximum_drop: float
|
|
277
|
+
max_iterations: int
|
|
278
|
+
gravity_constant: float
|
|
279
|
+
minimum_altitude: float
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@dataclass(slots=True)
|
|
283
|
+
class ShotProps:
|
|
284
|
+
bc: float
|
|
285
|
+
weight_grain: float
|
|
286
|
+
diameter_inch: float
|
|
287
|
+
length_inch: float
|
|
288
|
+
muzzle_velocity_fps: float
|
|
289
|
+
sight_height_ft: float
|
|
290
|
+
twist_inch: float
|
|
291
|
+
temp_c: float
|
|
292
|
+
pressure_hpa: float
|
|
293
|
+
altitude_ft: float
|
|
294
|
+
humidity: float
|
|
295
|
+
look_angle_rad: float
|
|
296
|
+
barrel_elevation_rad: float
|
|
297
|
+
barrel_azimuth_rad: float
|
|
298
|
+
cant_angle_rad: float
|
|
299
|
+
latitude_deg: float
|
|
300
|
+
azimuth_deg: float
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@dataclass(slots=True)
|
|
304
|
+
class ShotFields:
|
|
305
|
+
props: ShotProps
|
|
306
|
+
cfg: ConfigFields
|
|
307
|
+
drag_type: int
|
|
308
|
+
winds: list[WindFields]
|
|
309
|
+
drag_mach: list[float] | None # custom drag table (DRAG_CUSTOM), else None
|
|
310
|
+
drag_cd: list[float] | None
|
|
311
|
+
|
|
312
|
+
@property
|
|
313
|
+
def wind_count(self) -> int:
|
|
314
|
+
return len(self.winds)
|
|
315
|
+
|
|
316
|
+
@property
|
|
317
|
+
def drag_count(self) -> int:
|
|
318
|
+
return len(self.drag_mach) if self.drag_mach else 0
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@dataclass(slots=True)
|
|
322
|
+
class RequestFields:
|
|
323
|
+
range_limit_ft: float
|
|
324
|
+
range_step_ft: float
|
|
325
|
+
time_step: float
|
|
326
|
+
filter_flags: int
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class WindData(NamedTuple):
|
|
330
|
+
"""What Wind() returns (natmod's `Wind` namedtuple)."""
|
|
331
|
+
|
|
332
|
+
buf: None
|
|
333
|
+
s: WindFields
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class ConfigData(NamedTuple):
|
|
337
|
+
"""What Config() returns (natmod's `Config` namedtuple)."""
|
|
338
|
+
|
|
339
|
+
buf: None
|
|
340
|
+
s: ConfigFields
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class ShotData(NamedTuple):
|
|
344
|
+
"""What Shot() returns (natmod's `Shot` namedtuple)."""
|
|
345
|
+
|
|
346
|
+
buf: None
|
|
347
|
+
s: ShotFields
|
|
348
|
+
holder: None
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class RequestData(NamedTuple):
|
|
352
|
+
"""What Request() returns (natmod's `Request` namedtuple)."""
|
|
353
|
+
|
|
354
|
+
buf: None
|
|
355
|
+
s: RequestFields
|
|
356
|
+
traj: None
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def Wind(
|
|
360
|
+
velocity_fps: float = 0.0,
|
|
361
|
+
direction_from_rad: float = 0.0,
|
|
362
|
+
until_distance_ft: float = _INF,
|
|
363
|
+
max_distance_ft: float = _INF,
|
|
364
|
+
) -> WindData:
|
|
365
|
+
return WindData(
|
|
366
|
+
None,
|
|
367
|
+
WindFields(
|
|
368
|
+
velocity_fps=float(velocity_fps),
|
|
369
|
+
direction_from_rad=float(direction_from_rad),
|
|
370
|
+
until_distance_ft=float(until_distance_ft),
|
|
371
|
+
max_distance_ft=float(max_distance_ft),
|
|
372
|
+
),
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def Config(
|
|
377
|
+
step_multiplier: float = 0.5,
|
|
378
|
+
zero_finding_accuracy: float = 0.001,
|
|
379
|
+
minimum_velocity: float = 50.0,
|
|
380
|
+
maximum_drop: float = -15000.0,
|
|
381
|
+
max_iterations: int = 50,
|
|
382
|
+
gravity_constant: float = -32.17405,
|
|
383
|
+
minimum_altitude: float = -1500.0,
|
|
384
|
+
) -> ConfigData:
|
|
385
|
+
return ConfigData(
|
|
386
|
+
None,
|
|
387
|
+
ConfigFields(
|
|
388
|
+
step_multiplier=float(step_multiplier),
|
|
389
|
+
zero_finding_accuracy=float(zero_finding_accuracy),
|
|
390
|
+
minimum_velocity=float(minimum_velocity),
|
|
391
|
+
maximum_drop=float(maximum_drop),
|
|
392
|
+
max_iterations=int(max_iterations),
|
|
393
|
+
gravity_constant=float(gravity_constant),
|
|
394
|
+
minimum_altitude=float(minimum_altitude),
|
|
395
|
+
),
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _floats(column: DragColumn, count: int) -> list[float]:
|
|
400
|
+
if isinstance(column, (bytes, bytearray, memoryview)):
|
|
401
|
+
return list(_struct.unpack_from(f"<{count}f", column))
|
|
402
|
+
return [float(column[i]) for i in range(count)]
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def Shot(
|
|
406
|
+
bc: float = 0.0,
|
|
407
|
+
weight_grain: float = 0.0,
|
|
408
|
+
diameter_inch: float = 0.0,
|
|
409
|
+
length_inch: float = 0.0,
|
|
410
|
+
muzzle_velocity_fps: float = 0.0,
|
|
411
|
+
sight_height_ft: float = 0.0,
|
|
412
|
+
twist_inch: float = 0.0,
|
|
413
|
+
temp_c: float = 15.0,
|
|
414
|
+
pressure_hpa: float = 1013.25,
|
|
415
|
+
altitude_ft: float = 0.0,
|
|
416
|
+
humidity: float = 0.5,
|
|
417
|
+
look_angle_rad: float = 0.0,
|
|
418
|
+
barrel_elevation_rad: float = 0.0,
|
|
419
|
+
barrel_azimuth_rad: float = 0.0,
|
|
420
|
+
cant_angle_rad: float = 0.0,
|
|
421
|
+
latitude_deg: float = _NaN,
|
|
422
|
+
azimuth_deg: float = _NaN,
|
|
423
|
+
drag_type: int = DRAG_G7,
|
|
424
|
+
drag_mach: DragColumn | None = None,
|
|
425
|
+
drag_cd: DragColumn | None = None,
|
|
426
|
+
drag_count: int | None = None,
|
|
427
|
+
winds: Iterable[WindData] | None = None,
|
|
428
|
+
config: ConfigData | None = None,
|
|
429
|
+
) -> ShotData:
|
|
430
|
+
cfg = config if config is not None else Config()
|
|
431
|
+
wind_list = list(winds or [])[:_MAX_WINDS]
|
|
432
|
+
mach: list[float] | None = None
|
|
433
|
+
cd: list[float] | None = None
|
|
434
|
+
if drag_type == DRAG_CUSTOM and drag_mach and drag_cd:
|
|
435
|
+
if drag_count is not None:
|
|
436
|
+
dc = drag_count
|
|
437
|
+
elif isinstance(drag_mach, (bytes, bytearray, memoryview)):
|
|
438
|
+
dc = min(len(drag_mach) // 4, len(drag_cd) // 4)
|
|
439
|
+
else:
|
|
440
|
+
dc = min(len(drag_mach), len(drag_cd))
|
|
441
|
+
dc = min(dc, _MAX_DRAG_PTS)
|
|
442
|
+
mach, cd = _floats(drag_mach, dc), _floats(drag_cd, dc)
|
|
443
|
+
props = ShotProps(
|
|
444
|
+
bc=float(bc),
|
|
445
|
+
weight_grain=float(weight_grain),
|
|
446
|
+
diameter_inch=float(diameter_inch),
|
|
447
|
+
length_inch=float(length_inch),
|
|
448
|
+
muzzle_velocity_fps=float(muzzle_velocity_fps),
|
|
449
|
+
sight_height_ft=float(sight_height_ft),
|
|
450
|
+
twist_inch=float(twist_inch),
|
|
451
|
+
temp_c=float(temp_c),
|
|
452
|
+
pressure_hpa=float(pressure_hpa),
|
|
453
|
+
altitude_ft=float(altitude_ft),
|
|
454
|
+
humidity=float(humidity),
|
|
455
|
+
look_angle_rad=float(look_angle_rad),
|
|
456
|
+
barrel_elevation_rad=float(barrel_elevation_rad),
|
|
457
|
+
barrel_azimuth_rad=float(barrel_azimuth_rad),
|
|
458
|
+
cant_angle_rad=float(cant_angle_rad),
|
|
459
|
+
latitude_deg=float(latitude_deg),
|
|
460
|
+
azimuth_deg=float(azimuth_deg),
|
|
461
|
+
)
|
|
462
|
+
s = ShotFields(
|
|
463
|
+
props=props,
|
|
464
|
+
cfg=cfg.s,
|
|
465
|
+
drag_type=drag_type,
|
|
466
|
+
winds=[w.s for w in wind_list],
|
|
467
|
+
drag_mach=mach,
|
|
468
|
+
drag_cd=cd,
|
|
469
|
+
)
|
|
470
|
+
return ShotData(None, s, None)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def Request(
|
|
474
|
+
range_limit_ft: float = 3000.0,
|
|
475
|
+
range_step_ft: float = 100.0,
|
|
476
|
+
time_step: float = 0.0,
|
|
477
|
+
filter_flags: int = TRAJ_FLAG_RANGE,
|
|
478
|
+
) -> RequestData:
|
|
479
|
+
return RequestData(
|
|
480
|
+
None,
|
|
481
|
+
RequestFields(
|
|
482
|
+
range_limit_ft=float(range_limit_ft),
|
|
483
|
+
range_step_ft=float(range_step_ft),
|
|
484
|
+
time_step=float(time_step),
|
|
485
|
+
filter_flags=int(filter_flags),
|
|
486
|
+
),
|
|
487
|
+
None,
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _serialize(shot: ShotData) -> list[float]:
|
|
492
|
+
"""Flatten a Shot into tiny_bclibc_wasm.c's input layout."""
|
|
493
|
+
s = shot.s
|
|
494
|
+
p = s.props
|
|
495
|
+
c = s.cfg
|
|
496
|
+
mach: Sequence[float]
|
|
497
|
+
cd: Sequence[float]
|
|
498
|
+
if s.drag_type == DRAG_CUSTOM and s.drag_mach and s.drag_cd:
|
|
499
|
+
mach, cd = s.drag_mach, s.drag_cd
|
|
500
|
+
elif s.drag_type == DRAG_G1:
|
|
501
|
+
mach, cd = _drag_tables.G1_MACH, _drag_tables.G1_CD
|
|
502
|
+
else:
|
|
503
|
+
mach, cd = _drag_tables.G7_MACH, _drag_tables.G7_CD
|
|
504
|
+
values = [
|
|
505
|
+
p.bc,
|
|
506
|
+
p.weight_grain,
|
|
507
|
+
p.diameter_inch,
|
|
508
|
+
p.length_inch,
|
|
509
|
+
p.muzzle_velocity_fps,
|
|
510
|
+
p.sight_height_ft,
|
|
511
|
+
p.twist_inch,
|
|
512
|
+
p.temp_c,
|
|
513
|
+
p.pressure_hpa,
|
|
514
|
+
p.altitude_ft,
|
|
515
|
+
p.humidity,
|
|
516
|
+
p.look_angle_rad,
|
|
517
|
+
p.barrel_elevation_rad,
|
|
518
|
+
p.barrel_azimuth_rad,
|
|
519
|
+
p.cant_angle_rad,
|
|
520
|
+
p.latitude_deg,
|
|
521
|
+
p.azimuth_deg,
|
|
522
|
+
c.step_multiplier,
|
|
523
|
+
c.zero_finding_accuracy,
|
|
524
|
+
c.minimum_velocity,
|
|
525
|
+
c.maximum_drop,
|
|
526
|
+
float(c.max_iterations),
|
|
527
|
+
c.gravity_constant,
|
|
528
|
+
c.minimum_altitude,
|
|
529
|
+
float(len(mach)),
|
|
530
|
+
float(len(s.winds)),
|
|
531
|
+
]
|
|
532
|
+
values.extend(mach)
|
|
533
|
+
values.extend(cd)
|
|
534
|
+
for w in s.winds:
|
|
535
|
+
values.extend((w.velocity_fps, w.direction_from_rad, w.until_distance_ft, w.max_distance_ft))
|
|
536
|
+
return values
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _row(v: Sequence[float], i: int) -> Row:
|
|
540
|
+
"""One TrajectoryData row as natmod's 16-tuple (15 floats + int flag)."""
|
|
541
|
+
return (
|
|
542
|
+
v[i],
|
|
543
|
+
v[i + 1],
|
|
544
|
+
v[i + 2],
|
|
545
|
+
v[i + 3],
|
|
546
|
+
v[i + 4],
|
|
547
|
+
v[i + 5],
|
|
548
|
+
v[i + 6],
|
|
549
|
+
v[i + 7],
|
|
550
|
+
v[i + 8],
|
|
551
|
+
v[i + 9],
|
|
552
|
+
v[i + 10],
|
|
553
|
+
v[i + 11],
|
|
554
|
+
v[i + 12],
|
|
555
|
+
v[i + 13],
|
|
556
|
+
v[i + 14],
|
|
557
|
+
int(v[i + 15]),
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
# ── API ───────────────────────────────────────────────────────────────────────
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
class Trajectory(NamedTuple):
|
|
565
|
+
"""Everything one integration returns (see integrate_ex)."""
|
|
566
|
+
|
|
567
|
+
rows: list[Row]
|
|
568
|
+
reason: int # TINY_BCLIBC_TerminationReason
|
|
569
|
+
total: int # rows the solver emitted (== len(rows) unless the output was cut short)
|
|
570
|
+
final: RawState # the exact terminal state, whether or not a row was emitted for it
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def integrate(shot: ShotData, req: RequestData) -> tuple[list[Row], int]:
|
|
574
|
+
"""Return ``(rows, stop_reason)``; each row is a 16-tuple indexed by the ``T_*`` constants."""
|
|
575
|
+
traj = integrate_ex(shot, req)
|
|
576
|
+
return traj.rows, traj.reason
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def integrate_ex(shot: ShotData, req: RequestData) -> Trajectory:
|
|
580
|
+
"""Like integrate(), plus the row total and the terminal raw state (not part of the natmod API).
|
|
581
|
+
|
|
582
|
+
The terminal state lets a caller close a trajectory that ended other than by reaching the
|
|
583
|
+
requested range with its exact last point.
|
|
584
|
+
"""
|
|
585
|
+
r = req.s
|
|
586
|
+
out = _call(
|
|
587
|
+
"integrate",
|
|
588
|
+
"tbw_integrate",
|
|
589
|
+
_serialize(shot),
|
|
590
|
+
r.range_limit_ft,
|
|
591
|
+
r.range_step_ft,
|
|
592
|
+
r.time_step,
|
|
593
|
+
r.filter_flags,
|
|
594
|
+
)
|
|
595
|
+
n = int(out[3])
|
|
596
|
+
final: RawState = (out[4], out[5], out[6], out[7], out[8], out[9], out[10], out[11])
|
|
597
|
+
rows = [_row(out, _INTEGRATE_HEADER + k * _ROW) for k in range(n)]
|
|
598
|
+
return Trajectory(rows, int(out[1]), int(out[2]), final)
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def integrate_stream(shot: ShotData, req: RequestData, cb: Callable[[Row], object]) -> tuple[int, int]:
|
|
602
|
+
"""Call ``cb(row)`` per row; a truthy return stops early. Returns ``(count, stop_reason)``.
|
|
603
|
+
|
|
604
|
+
The module computes the whole trajectory in one call and the callbacks run afterwards (one
|
|
605
|
+
host round trip instead of one per row); a stop therefore reports reason 5 (handler stop)
|
|
606
|
+
exactly like the natmod, but does not save the remaining integration work.
|
|
607
|
+
"""
|
|
608
|
+
rows, reason = integrate(shot, req)
|
|
609
|
+
for i, row in enumerate(rows):
|
|
610
|
+
if cb(row):
|
|
611
|
+
return i + 1, _TERM_HANDLER_STOP
|
|
612
|
+
return len(rows), reason
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def integrate_at(shot: ShotData, interp: int, val: float) -> tuple[RawState, Row]:
|
|
616
|
+
"""Return ``(raw, row)`` where the ``INTERP_*`` quantity equals val.
|
|
617
|
+
|
|
618
|
+
``raw`` is (time, px, py, pz, vx, vy, vz, mach); ``row`` a 16-tuple.
|
|
619
|
+
"""
|
|
620
|
+
o = _call("integrate_at", "tbw_integrate_at", _serialize(shot), int(interp), float(val))
|
|
621
|
+
raw: RawState = (o[1], o[2], o[3], o[4], o[5], o[6], o[7], o[8])
|
|
622
|
+
return raw, _row(o, _AT_HEADER)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def find_zero_angle(shot: ShotData, dist_ft: float) -> float:
|
|
626
|
+
"""Barrel elevation (rad) that zeroes the shot at dist_ft."""
|
|
627
|
+
return _call("find_zero_angle", "tbw_find_zero_angle", _serialize(shot), float(dist_ft))[1]
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def zero_point(shot: ShotData, dist_ft: float) -> tuple[float, Row]:
|
|
631
|
+
"""Return the solver's ``(zero_angle_rad, terminal_row)`` without re-integration."""
|
|
632
|
+
out = _call("zero_point", "tbw_find_zero_point", _serialize(shot), float(dist_ft))
|
|
633
|
+
return out[1], _row(out, 2)
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def zero(shot: ShotData, dist_ft: float) -> float:
|
|
637
|
+
"""Set ``shot``'s barrel elevation for dist_ft and return it in radians."""
|
|
638
|
+
angle, _point = zero_point(shot, dist_ft)
|
|
639
|
+
shot.s.props.barrel_elevation_rad = angle
|
|
640
|
+
return angle
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def aim(shot: ShotData, dist_ft: float) -> tuple[float, float, Row]:
|
|
644
|
+
"""Return ``(vertical_hold_rad, windage_rad, point)`` for a target distance.
|
|
645
|
+
|
|
646
|
+
The hold is relative to the barrel elevation currently stored in ``shot`` (normally set by
|
|
647
|
+
:func:`zero`).
|
|
648
|
+
"""
|
|
649
|
+
angle, point = zero_point(shot, dist_ft)
|
|
650
|
+
return angle - shot.s.props.barrel_elevation_rad, point[T_WINDAGE_ANGLE], point
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def fire(shot: ShotData, req: RequestData) -> tuple[list[Row], int]:
|
|
654
|
+
"""Calculate and return ``(trajectory_rows, stop_reason)``."""
|
|
655
|
+
return integrate(shot, req)
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def find_apex(shot: ShotData) -> Row:
|
|
659
|
+
"""The apex (vertical velocity = 0) as a 16-tuple row."""
|
|
660
|
+
return _row(_call("find_apex", "tbw_find_apex", _serialize(shot)), 1)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def find_max_range(shot: ShotData, lo: float, hi: float) -> tuple[float, float]:
|
|
664
|
+
"""Return ``(max_range_ft, angle_rad)`` searched between lo and hi degrees."""
|
|
665
|
+
out = _call("find_max_range", "tbw_find_max_range", _serialize(shot), float(lo), float(hi))
|
|
666
|
+
return out[1], out[2]
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
# ── MultiBC ───────────────────────────────────────────────────────────────────
|
|
670
|
+
# Same algorithm as natmod's build_multibc (src/tiny_bclibc_mp.c) and ffimod's pure-Python port.
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def _interp_bc(bc_mach: Sequence[float], bc_val: Sequence[float], mach: float) -> float:
|
|
674
|
+
n = len(bc_mach)
|
|
675
|
+
if mach <= bc_mach[0]:
|
|
676
|
+
return bc_val[0]
|
|
677
|
+
if mach >= bc_mach[n - 1]:
|
|
678
|
+
return bc_val[n - 1]
|
|
679
|
+
lo, hi = 0, n - 1
|
|
680
|
+
while hi - lo > 1:
|
|
681
|
+
mid = (lo + hi) // 2
|
|
682
|
+
if bc_mach[mid] <= mach:
|
|
683
|
+
lo = mid
|
|
684
|
+
else:
|
|
685
|
+
hi = mid
|
|
686
|
+
t = (mach - bc_mach[lo]) / (bc_mach[hi] - bc_mach[lo])
|
|
687
|
+
return bc_val[lo] + t * (bc_val[hi] - bc_val[lo])
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def build_multibc(
|
|
691
|
+
drag_type: int,
|
|
692
|
+
bc_points_buf: bytes | bytearray | memoryview,
|
|
693
|
+
out_mach_buf: bytearray | memoryview,
|
|
694
|
+
out_cd_buf: bytearray | memoryview,
|
|
695
|
+
) -> int:
|
|
696
|
+
"""Low-level primitive with natmod's signature: packed "<ff" (mach, bc) points in, float32 out."""
|
|
697
|
+
n_pts = len(bc_points_buf) // 8
|
|
698
|
+
pts: list[tuple[float, float]] = sorted(
|
|
699
|
+
(_struct.unpack_from("<ff", bc_points_buf, i * 8) for i in range(n_pts)),
|
|
700
|
+
key=lambda p: p[0],
|
|
701
|
+
)
|
|
702
|
+
bc_mach = [p[0] for p in pts]
|
|
703
|
+
bc_val = [p[1] for p in pts]
|
|
704
|
+
# natmod: 0 = G1, anything else = G7
|
|
705
|
+
if drag_type == DRAG_G1:
|
|
706
|
+
ref_mach, ref_cd = _drag_tables.G1_MACH, _drag_tables.G1_CD
|
|
707
|
+
else:
|
|
708
|
+
ref_mach, ref_cd = _drag_tables.G7_MACH, _drag_tables.G7_CD
|
|
709
|
+
n = len(ref_mach)
|
|
710
|
+
for i in range(n):
|
|
711
|
+
bc_at = _interp_bc(bc_mach, bc_val, ref_mach[i])
|
|
712
|
+
_struct.pack_into("<f", out_mach_buf, i * 4, ref_mach[i])
|
|
713
|
+
_struct.pack_into("<f", out_cd_buf, i * 4, ref_cd[i] / bc_at)
|
|
714
|
+
return n
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def MultiBC(bc_points: Iterable[tuple[float, float]], drag_type: int = DRAG_G7) -> tuple[bytearray, bytearray, int]:
|
|
718
|
+
"""Fold (mach, bc) points into one custom drag curve: returns ``(mach_buf, cd_buf, count)``.
|
|
719
|
+
|
|
720
|
+
Feed it to ``Shot(bc=1.0, drag_type=DRAG_CUSTOM, drag_mach=mach_buf, drag_cd=cd_buf,
|
|
721
|
+
drag_count=count)`` -- same contract as the natmod's MultiBC().
|
|
722
|
+
"""
|
|
723
|
+
pts = list(bc_points)
|
|
724
|
+
pts_buf = bytearray(len(pts) * 8)
|
|
725
|
+
for i, (mach, bc_val) in enumerate(pts):
|
|
726
|
+
_struct.pack_into("<ff", pts_buf, i * 8, mach, bc_val)
|
|
727
|
+
mach_buf = bytearray(_MAX_DRAG_PTS * 4)
|
|
728
|
+
cd_buf = bytearray(_MAX_DRAG_PTS * 4)
|
|
729
|
+
count = build_multibc(drag_type, pts_buf, mach_buf, cd_buf)
|
|
730
|
+
return mach_buf, cd_buf, count
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
# ── bench: FPU micro-benchmark ────────────────────────────────────────────────
|
|
734
|
+
# The natmod's bench() runs native FPU loops; these are the same loops compiled into the module
|
|
735
|
+
# (tbw_bench_*), so what is measured is how fast the WebAssembly host runs f32/f64 arithmetic --
|
|
736
|
+
# useful for comparing hosts (wasmtime vs wasm3 vs JavaScriptCore without its JIT, ...).
|
|
737
|
+
|
|
738
|
+
_BENCH_N_LAT: Final = 500_000 # x4 ops/iter
|
|
739
|
+
_BENCH_N_THR: Final = 100_000 # x16 ops/iter
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def _bench_run(label: str, export: str, n: int, ops: int) -> None:
|
|
743
|
+
runner = _get_runner()
|
|
744
|
+
runner.call_scalar(export, n // 10) # warmup
|
|
745
|
+
t0 = time.perf_counter()
|
|
746
|
+
runner.call_scalar(export, n)
|
|
747
|
+
dt = time.perf_counter() - t0
|
|
748
|
+
print(f" {label:8s}: {n * ops / dt / 1e6:9.2f} MFLOPS dt={dt:.3f}s")
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def bench() -> None:
|
|
752
|
+
"""Print an FPU latency/throughput/peak micro-benchmark (MFLOPS) of the WebAssembly host."""
|
|
753
|
+
print("=" * 52)
|
|
754
|
+
print(f"tiny_bclibc FPU FLOPS Benchmark (wasm on {host()})")
|
|
755
|
+
print("=" * 52)
|
|
756
|
+
print("\nLatency-bound (volatile, sequential chain):")
|
|
757
|
+
_bench_run("DP", "tbw_bench_lat_dp", _BENCH_N_LAT, 4)
|
|
758
|
+
_bench_run("SP", "tbw_bench_lat_sp", _BENCH_N_LAT, 4)
|
|
759
|
+
print("\nThroughput (8 independent accumulators, volatile operands):")
|
|
760
|
+
_bench_run("DP", "tbw_bench_thr_dp", _BENCH_N_THR, 16)
|
|
761
|
+
_bench_run("SP", "tbw_bench_thr_sp", _BENCH_N_THR, 16)
|
|
762
|
+
print("\nPeak (8 independent accumulators, register-resident, add-only):")
|
|
763
|
+
_bench_run("DP", "tbw_bench_peak_dp", _BENCH_N_THR, 8)
|
|
764
|
+
_bench_run("SP", "tbw_bench_peak_sp", _BENCH_N_THR, 8)
|
|
765
|
+
print("=" * 52)
|