Changelog¶
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased¶
2.3.1 - 2026-07-06¶
Added¶
- Native
py_ballisticcalc.extswheels for Android (cp313-android_*/cp314-android_*,arm64_v8a+x86_64) via a newcibuildwheel[android]-driven CI job —.github/workflows/pypi-publish.yml,[tool.cibuildwheel.android]inpy_ballisticcalc.exts/pyproject.toml(#339) - Native
py_ballisticcalc.extswheels for Pyodide/WebAssembly (cp313-pyodide_*/cp314-pyodide_*,wasm32) viacibuildwheel[pyodide], with its own~/.cache/cibuildwheelcache keyed onpyproject.toml's hash —[tool.cibuildwheel.pyodide]inpy_ballisticcalc.exts/pyproject.toml,.github/workflows/pypi-publish.yml(#340); confirmed against the actual2.3.1b2PyPI release:cp313built against Pyodide xbuildenv0.29.4(wheel tagcp313-cp313-pyemscripten_2025_0_wasm32),cp314against a newer, not-yet-stable xbuildenv (tagcp314-cp314-pyemscripten_2026_0_wasm32) — each Pyodide release pins one exact CPython+Emscripten ABI (seesetup.pycomment below), so a wheel only installs under the matching Pyodide version examples/pyodide/index.html: standalone, browser-only interactive REPL demoingpy_ballisticcalc/py_ballisticcalc.extsrunning entirely client-side under Pyodide — no server, no install. Rebuilds the official Pyodideconsole.htmlterminal (jQuery Terminal +pyodide.console.PyodideConsole, top-levelawait,Ctrl+C, tab-completion) pinned to the Pyodide build matching thecp313wasm32 wheel above, then on load feeds a.338LM/300gr SMK example (micropip.install(..., pre=True)since2.3.1b2is a pre-release →cythonized_rk4_enginezero + 1000 m trajectory) into the live console as a normal, narrated transcript rather than running it silently, leavingcalc/shot/weapon/ammo/resultin scope for further exploration; four<script src>tags moved from<head>to the end of<body>so the loading spinner paints immediately instead of being blocked behind the Pyodide/jQuery Terminal downloadsdocs/pyodide(symlink toexamples/pyodide/,not_in_nav: pyodide/*inmkdocs.ymlto silence the resulting "omitted from nav" warning) and a new "Interactive Web REPL" nav entry linking straight to the rawpyodide/index.htmlpage (deliberately not wrapped in a.mddoc page or an<iframe>: an iframe embed of a full Pyodide runtime was noticeably laggy, and a raw asset is excluded fromsitemap.xml, which keeps Material'snavigation.instant.previewhover-card from trying to render it) —mkdocs.yml,docs/index.md,README.md[![powered by pyodide]][Pyodide]badge inREADME.md, alongside a new (previously unbadged)[![powered by bclibc]][bclibc]— both use a base64-embedded logo (bclibc: its own circular mark; pyodide: the white-on-transparentlogo-quadratic-dark.svgfrom pyodide/pyodide-artwork, chosen over the wordmarklogo-dark.svg/logo-light.svgvariants because shields.io badge logos need a roughly square aspect ratio) so the icon renders standalone without depending on an external stylesheet or the shields.io logo-service's own icon set
Changed¶
py_ballisticcalc.exts/setup.py: cross-compile target detection (IS_EMSCRIPTEN/IS_ANDROID/IS_IOS, viasysconfig.get_platform()rather thanplatform.system(), which reports the host OS under cross-compilation) now also disables the stable-ABI (abi3) build for Android and iOS, not just Emscripten — Android's Bionicdlopenrequires every dependency to resolve to a literallibpythonX.Y.soat load time (no lazy glibc-style symbol resolution), so anabi3wheel only looks version-independent while actually hardcoding a dependency on one CPython point release; confirmed empirically (acp311-abi3-androidwheel built against acp313Android crossenv failed to import on 3.14 withlibrary libpython3.13.so not found); also adds an Emscripten-specific compiler-flags branch (-std=c99/-std=c++11, noCC/CXXoverride sincepyodide-buildalready setsemcc/em++, no-Wl,-strip-allsinceem++'s linker wrapper doesn't reliably support it) (#339).github/workflows/pypi-publish.ymlwheel-build matrix: every entry now carries a descriptivename(shown in the Actions UI as "Build wheels on ubuntu-latest / Pyodide WebAssembly" etc.), anartifact_key(upload-artifact names are nowdist-wheels-<key>instead ofdist-<os>, since Android/Pyodide/Linux-x64 all shareos: ubuntu-latest), and an explicitcibw_platform(--platform ${{ matrix.cibw_platform || 'auto' }}, output moved from./distto./wheelhouseto avoid colliding with the sdist job's./dist); macOS runners bumpedmacos-15/macos-15-intel→macos-26/macos-26-intel(also inpytest-{rk4,cythonized-rk4,scipy}-engine.yml, plus added to thepytest-manual.ymlworkflow_dispatchOS dropdown);SETUPTOOLS_SCM_OVERRIDESenv var (and matchingCIBW_ENVIRONMENT_PASSentry, needed for container-based builds like manylinux which don't otherwise inherit the host env) now setslocal_scheme = "no-local-version"on push/TestPyPI runs, since PyPI/TestPyPI reject local version segments (+g<hash>) that build-only runs keep for traceability (#339).github/dependabot.yml: the two separateuvecosystem blocks (root +py_ballisticcalc.exts/) merged into one block withdirectories: ["/", "py_ballisticcalc.exts/"]; the freed-up per-py_ballisticcalc.extsblock repurposed to annpmecosystem entry watchingpy_ballisticcalc.exts/pyodide/(the pyodide/emscripten cross-build toolchain pulls in node/npm dependencies) (#340).gitignore:**/wheelhouse(newcibuildwheeloutput dir, see above),**/.pyodide_build,**/node_modules/(#340)README.md:[![pyright]]badge replaced by[![Pre-commit]]— pyright has run as a.pre-commit-config.yamlhook via the consolidatedpre-commit.ymlworkflow since2.3.0, so the standalonepyright.ymlbadge/workflow link was already stale; engine-comparison table column widths realigned (padding only, no content change — had gone ragged after2.3.0added thecythonized_verlet_enginerow without matching the other rows' width)
CI¶
.github/workflows/pypi-publish.yml:Cache cibuildwheel/pyodide toolchainstep (actions/cache@v6on~/.cache/cibuildwheel) added for the Pyodide job only — the Pyodide xbuildenv/Emscripten SDK download is the slowest part of that build and doesn't change between runs unlesspy_ballisticcalc.exts/pyproject.tomldoes (#340)
2.3.0 - 2026-07-24¶
Added¶
cythonized_verlet_engine(py_ballisticcalc_exts.CythonizedVelocityVerletIntegrationEngine): compiled counterpart to the existing pure-Pythonverlet_engine, wired up the same way ascythonized_rk4_engine/cythonized_euler_engine(velocity_verlet_engine.pxd/.pyx/.pyi,setup.pysource/dependency registration,py_ballisticcalc_exts/__init__.pyexport,[project.entry-points.py_ballisticcalc]inpy_ballisticcalc.exts/pyproject.toml) against bclibc'sBCLIBC_integrateVELOCITY_VERLET(include/bclibc/velocity_verlet.hpp, released in bclibcv1.1.7); produces identical trajectories toverlet_enginebit-for-bit on a standard test shot and passes the fullpytestsuite (373 passed, 2 skipped) andpy_ballisticcalc.exts/tests; benchmarked at 100x (Trajectory) / 157x (Find Zero) faster thanrk4_engine— added to the engine comparison table inREADME.md/docs/concepts/engines.md.github/workflows/pytest-cythonized-verlet-engine.yml: minimal-matrix CI (ubuntu-latest×3.11, push + PR) forcythonized_verlet_engine, mirroringpytest-cythonized-euler-engine.yml— no full cross-OS/version matrix since it shares its integration/data-structure core withcythonized_rk4_engine(which keeps the full matrix);README.mdbadge added.github/workflows/pytest-manual.yml: addedcythonized_verlet_engineto theengine_nameworkflow_dispatchdropdown (itsoptions:list is hand-maintained, unlikepytest-reusable.yml/ci-helpers.sh, which key off a genericcythonized_*prefix and needed no change);.github/workflows/coverage.ymlalso needed no change — it discovers engines dynamically viaCalculator.iter_engines(), confirmed by simulating itsdiscover-engines/teststeps locally (engine listed,--extra extsinstalled, coverage run passes)
Changed¶
- bclibc submodule bumped to
v1.1.7— adds thevelocity_verletintegration method (see Added) .pylintrc(643 lines, essentially unmodifiedpylint --generate-rcfileoutput) replaced by[tool.pylint.main]/[tool.pylint.design]/[tool.pylint."messages control"]inpyproject.toml; diffed against a freshly generated default rcfile of the same pylint version to isolate the actual overrides (fail-under=9.0vs default9.5,max-positional-arguments=6vs default5,ignore,py-version, and thedisable=list — which, despite reading like--generate-rcfile's own suggested defaults, is not applied unless present in a config file: without it, pylint's score onpy_ballisticcalcdrops from 9.57/10 to 7.89/10); verified byte-for-byte equivalent pylint findings/score before deleting.pylintrc[tool.ruff] extend-exclude(pyproject.toml) gainedpy_ballisticcalc.exts/py_ballisticcalc_exts/external/bclibc—ruff check --fixwas reaching into the vendored bclibc submodule (a separate upstream repo) and rewriting its typing (Optional[X]→X | None, etc.); confirmed[tool.ruff.lint].excludedoes not control file discovery the way top-levelexclude/extend-excludedo (ruff still walked and linted files under it when onlylint.excludewas set), so the fix went into the shared top-level exclude insteadpy_ballisticcalc/interface.py:from py_ballisticcalc import RK4IntegrationEngine→from py_ballisticcalc.engines import RK4IntegrationEngine— the former was a real (if currently-working) circular import:py_ballisticcalc/__init__.pyimportsinterface.py, which imported back from the partially-initializedpy_ballisticcalcpackage itself; it only worked because__init__.pyhappens to import.enginesbefore.interface, leaving the whole package's importability fragile to any future reordering of that file (caught by pylint'scyclic-import, whichruff'sPL*rule subset does not implement)py_ballisticcalc/engines/base_engine.py,rk4.py,engines/scipy_engine.py,unit.py,vector.py,generics/engine.py,interpolation.py,constants.py,drag_tables.py, and severalpy_ballisticcalc.extsscripts: broadruff check --fixcleanup — typing modernization (Optional[X]/List[X]/Dict[X]→X | None/list[X]/dict[X]),set()/tuple()literal rewrites,.keys()removal, nested-ifmerges (only where no attachedelifwould change semantics — a first attempt at mergingbase_engine.py'sZERO_UP/ZERO_DOWNcrossing-detection nesting did change semantics and was reverted after it brokeTestTrajectoryDataFilter.test_zero_up_then_zero_down_ordering),yield-loop →yield from,%-format → f-string,ClassVarannotations onUnit's per-subclass_conversion_factors(RUF012), and shebang scripts (cythonization-report.py,setup.py,uconv.py) marked executable (EXE001); a handful of findings were suppressed with# noqa+ rationale instead of "fixed" where the suggestion didn't match the code's actual intent:PreferredUnitsMeta.__repr__'sgetattr(cls, "__dataclass_fields__")(pyright can't see the attribute@dataclassinjects onto the metaclass at runtime — ruff's own--fixhad silently rewritten this to direct attribute access, which is what brokepyright),Vector.__iadd__/__isub__(immutableNamedTuple: genuinely returns a new instance, notself, contraPYI034),ConfigTingenerics/engine.py(public name re-exported fromgenerics/__init__.py, contraPLC0105's naming convention), and both__all__sequences inconstants.py/interpolation.py(deliberately grouped by category with comments, contraRUF022's alphabetical sort)py_ballisticcalc/engines/rk4.py,engines/scipy_engine.py: closures created inside per-iteration loops (acceleration()in RK4,x_minus_target()in SciPy's root-finding) now bind their loop-scoped variable (k_m,x_target) as a default argument instead of relying on the closure — both were already correct at runtime (fully invoked before the next iteration reassigns the variable) but trippedB023; the default-argument form makes that safety explicit rather than incidental
Fixed¶
.github/workflows/pre-commit.yml: checkout step was missingsubmodules: recursive, so the consolidated pre-commit workflow (see CI) failed itsuv-synchook on every run —py_ballisticcalc.exts' Cython build needsbclibc's vendored C++ sources, which a non-recursive checkout never fetches
Removed¶
.pylintrc— migrated intopyproject.toml(see Changed)
CI¶
.github/workflows/pyright.yml,ruff.yml,pylint.ymlremoved; all three now run as.pre-commit-config.yamlhooks (uv-pyright,uv-ruff-check,uv-ruff-format,uv-pylint) via the existing.github/workflows/pre-commit.yml(pre-commit run --all-files), instead of three separate workflows each reinstalling the project.pre-commit-config.yaml:uv-pylinthook scoped topy_ballisticcalconly (args: [..., "py_ballisticcalc"],pass_filenames: false) instead of linting whatever.py/.pyifiles changed anywhere in the repo with no path filter — the old form ran pylint againstpy_ballisticcalc.exts/tests/scripts that were never in its scope, always failing thefail-under=9.0gate locally (score dropped to 8.71) even for cleanpy_ballisticcalc-only changes.pre-commit-config.yaml: addeduv-lock-extshook (uv lock --upgrade --project py_ballisticcalc.exts), mirroring the existinguv-lockhook for the main package'suv.lock
2.3.0rc2 - 2026-07-21¶
Changed¶
- bclibc submodule bumped to
v1.1.5—BCLIBC_BaseTrajSeq::get_at()(src/traj_data.cpp) silently extrapolated instead of raising whenkey_valuewas outside the trajectory's range, sincebisect_center_idx_buf()/find_target_index()clamp their bracket index to[1, n-2]regardless of how farkey_valuefalls outside the sequence, andinterpolate_at()only ever validated the index, never the value;get_at()now checkskey_valueagainst[min(buffer[0][key], buffer[n-1][key]), max(...)](± a1e-9epsilon) before searching and throwsstd::out_of_range(surfaces as PythonIndexError) if it falls outside (bclibc#19); also fixes a broken artifact-path check inpre-commit-check.shand adds a CTest-based C++ unit test target — surfaced while investigating #305 below
Fixed¶
HitResult.get_at()(trajectory_data.py): raisedArithmeticError: Trajectory does not reach ...when the requested value was a floating-point epsilon beyond an existing trajectory point (e.g. from meter→inch conversion artifacts) instead of returning that point; the forward-search loop's bracket condition (curr_val < key_value <= next_val) excludes anykey_valuepast the bracket it can find, sotarget_idxstayed-1and the error was raised before theepsiloncheck could ever run, regardless of how largeepsilonwas passed — this affected not just the first/last trajectory points but also interior local extrema (e.g. the trajectory apex) for non-monotonic attributes like height, since an extremum epsilon-overshoot falls outside every bracket the same way an endpoint epsilon-overshoot does; fix: ontarget_idx == -1, scan the whole trajectory for its closest point by key-attribute value (min(traj, key=...)) and return it if withinepsilon— only reached on this error-fallback path, so the O(n) scan has no cost on the normal path — resolves #305; pure-Python fix —py_ballisticcalc_exts' internalCythonizedBaseTrajSeq.get_at()(bclibc) uses a different binary-search-and-always-interpolate algorithm not affected by this specific bug, but investigating it surfaced the opposite problem there (see bclibc submodule bump tov1.1.5above)
Tests¶
TestIssue305added totests/test_issues.py— regression tests forHitResult.get_at(): a value a float-epsilon past the last trajectory point resolves to that last point, a value a float-epsilon before the first point resolves to the first point, a value a float-epsilon past an interior local extremum (trajectory apex) resolves to that extremum, and a value genuinely far out of range still correctly raisesArithmeticErrorpy_ballisticcalc.exts/tests/test_get_at_functions.py: addedCythonizedBaseTrajSeq.get_at()regression tests mirroringTestIssue305for the Cython/bclibc path — epsilon past/before the first and last points still resolve to those points, and values genuinely far out of range now raiseIndexErrorper the bclibcv1.1.5fix
CI¶
.github/workflows/coverage.yml: added top-levelconcurrency: {group: "${{ github.workflow }}-${{ github.ref }}", cancel-in-progress: true}, matching every other workflow in the repo — a new push/PR update now cancels the previous in-flight coverage run on the same ref instead of both running to completion.github/workflows/coverage.yml:discover-enginesandmergejobs now skip whengithub.actor == 'dependabot[bot]'(testskips too, transitively, via itsneeds: discover-engines) — dependency-bump PRs don't need a fresh full-matrix coverage run, andmerge's existingif: ${{ !cancelled() }}would otherwise still have attempted to run (and fail, with no coverage data to combine) even withdiscover-engines/testskipped, since a skipped job isn't a cancelled onepytest-euler-engine.yml,pytest-verlet-engine.yml,pytest-cythonized-euler-engine.yml:test_full_matrixjob (6-OS ×3.11/3.14/3.14tmatrix, PR-only) removed;test_minimal_matrix(ubuntu-latest×3.11only) now runs unconditionally on bothpushandpull_requestinstead of push-only — these three engines share their integration/data-structure code withrk4_engine/cythonized_rk4_engine(which keep the full cross-OS/version matrix), differing only in integration formula, so cross-platform/cross-version testing of that shared core would just be duplicated CI cost; the minimal matrix still catches regressions specific to each engine's own logic
2.3.0rc1 - 2026-07-20¶
Fixed¶
- CHANGELOG.md: the
v2.3.0b5Codecov entry'sif: ${{ !cancelled() }}GitHub Actions expression wasn't escaped for Jinja, so mkdocs/pymdown-extensionstried to evaluate it as a template expression when rendering the docs site; wrapped in...
2.3.0b5 - 2026-07-20¶
Added¶
- Codecov integration:
.github/workflows/coverage.ymlrewritten as a 3-job pipeline —discover-engines(lists engines viaCalculator.iter_engines()),test(matrix over discovered engines, each writing an isolated.coverage.<engine>data file),merge(coverage combine/xml/html, thencodecov/codecov-action@v7upload);mergeruns even if one engine's tests fail (if: ${{ !cancelled() }}) so coverage is still reported CODECOV_TOKENrepository secret added; README[coverage]badge now links to the live codecov.io report instead of the localcoverage.svg
Changed¶
_EngineLoader(interface.py):_get_entries_by_group()and new_load_by_name()wrapped infunctools.cache—entry_points().select(...)was re-scanning all installed distributions on every string-keyedCalculator(engine="...")construction (~4.4ms/call); now cached per-process (~0.0085ms/call on repeat calls, ~500x) — mainly benefits code that repeatedly loads an engine by name, e.g. the class-scopedloaded_engine_instancepytest fixture andCalculator.__setstate__on unpickle- Dependencies update to fix CVEs
Fixed¶
ScipyIntegrationEngine(engines/scipy_engine.py): 12 pyrightreportPossiblyUnboundVariablefindings onnp/root_scalar/minimize_scalar/solve_ivp— these are optional third-party imports (try/except ImportError), but every use is only reachable afterSciPyIntegrationEngine.__init__has already verified_HAS_NUMPY/_HAS_SCIPY, an invariant the type checker can't see across function boundaries; fixed by also importing them unconditionally underif TYPE_CHECKING:(never executed at runtime, only informs the type checker that the names are bound) — no runtime behavior changeScipyWindSock.__init__(engines/scipy_engine.py):windsparameter was typedWind | Sequence[Wind | None], which (a) allowedNoneitems inside the sequence even though nothing ever produces or handles those, and (b) didn't even include bareNoneas a valid top-level value despite the method explicitly handlingwinds is None; corrected toWind | Sequence[Wind] | None, matching the real caller (props.winds, typedSequence[Wind]) and the existingNone-handling codeColoredFormatter.__init__(logger.py):styleparameter had no type annotation, so pyright widened it tostrand flagged thesuper().__init__()call as incompatible withlogging.Formatter'sLiteral['%', '{', '$']; annotated asLiteral["%", "{", "$"] = "%"GenericDimension.from_raw(unit.py:840): parameter was namedunit, whileTemperature.from_raw's override (unit.py:1242) and theGenericDimensionProtocoldeclaration both useunits; pyright flagged the override as LSP-incompatible (reportIncompatibleMethodOverride); renamed the base method's parameter tounitsto matchVelocityVerletIntegrationEngine._integrate(engines/velocity_verlet.py):data_filter.finalize()was called with no argument, defaultingtermination_reasontoNoneand silently no-opping (unlikerk4.py/euler.py, which passtermination_reasonthrough); on anyRangeErrorpath (MaximumDropReached,MinimumAltitudeReached, incomplete/vertical shots) the real final trajectory point was never appended, soHitResult.trajectory[-1]stayed pinned at the muzzle point (time=0.0); fixed by passingtermination_reasonintofinalize(), matching the other engines
Removed¶
coverage.svg(root) — was only ever regenerated by a commented-outcoverage-badgestep; the README[coverage]badge now points at codecov.io instead- Orphaned unused
[coverage]reference definition (pointing at a non-raw.githubusercontent.com, already-brokencoverage.svgURL) removed frompy_ballisticcalc.exts/README.md
CI¶
- Type checker swapped from
mypytopyright, recommended by Serhiy Yevtushenko:[tool.mypy]→[tool.pyright]inpyproject.toml(include = ["py_ballisticcalc"]);mypy>=2.3.0→pyright>=1.1.411in both dev dependency groups;.pre-commit-config.yamluv-mypyhook →uv-pyright;.github/workflows/mypy.ymlrenamed topyright.yml;docs/contributing.mdcommand updated; stale.mypy_cache/entries removed from both.gitignorefiles;.pylintrc'sspelling-ignore-comment-directivesmypy:→pyright:;# Make mypy happycomments inbase_engine.py/scipy_engine.pyreworded generically.reportUnsupportedDunderAlldisabled in[tool.pyright]—__init__.py/drag_tables.pybuild__all__dynamically fromglobals()/a generated list by design, which pyright can't statically verify .pre-commit-config.yaml:astral-sh/uv-pre-commit's pinneduv-lockhook (rev: 0.11.29) replaced with a locallanguage: systemhook runninguv lock --checkdirectly, placed right beforeuv-sync— avoids a second, independently-pinneduvversion diverging from whateveruvis actually used to run the rest of the hooks and CI.github/workflows/pytest-verlet-engine.ymladded —verlet_enginehad no dedicated pytest-*-engine.yml (onlycoverage.ymlexercised it); new file mirrors the euler/rk4/scipy pattern (full matrix onpull_request, minimal matrix onpush,fail-fast: false).github/workflows/pytest-manual.yml:verlet_engineadded to theengine_namechoice-input options.github/workflows/pytest-reusable.yml:astral-sh/setup-uvnow setsenable-cache: truewith an explicitcache-dependency-globcoveringpy_ballisticcalc.exts/**and.gitmodules(not justuv.lock/pyproject.toml) — a cache hit now skips rebuilding the Cython extension entirely when its source tree is unchanged from a prior run, e.g. acrosscythonized_euler_engine/cythonized_rk4_enginelegs on the same runner or on job re-runs/retriestest_full_matrix/test_minimal_matrixjobname:shortened toFull/Minimalacrosspytest-{euler,rk4,scipy,verlet}-engine.ymlandpytest-cythonized-{euler,rk4}-engine.yml; the two cythonized files'test_minimal_matrixpreviously had no explicitname:at all.gitignore:.coverageglob widened to.coverage*and**/coverage.xmladded, to also ignore per-engine coverage data files (.coverage.<engine>) and generated XML reports from the newcoverage.yml
2.3.0b4 - 2026-06-25¶
Changed¶
- bclibc submodule reference updated from
v1.1.2tov1.1.4— no additional wrapper-consolidation work of its own; picks up bclibc's interimv1.1.3fix (Ridder's-methodf_nextordering, seev2.3.0b3below) plus further upstreamv1.1.4changes
2.3.0b3 - 2026-06-22¶
Changed¶
py_ballisticcalc.exts(bind.pyx):BCLIBC_ShotProps_from_pyobjectis now a thin field mapper — fillsBCLIBC_Shotwith natural-unit values and delegates all physics/unit conversions toBCLIBC_Shot::to_shot_props()in C++ (cant cos/sin, CIPM-2007 atmosphere, Coriolis trig, PCHIP drag curve, wind sock assembly); Step 2 of bclibc-wrapper-consolidationpy_ballisticcalc.exts(base_types.pxd): addedBCLIBC_Shotcppclass declarationpy_ballisticcalc.exts(base_types.pxd):BCLIBC_Corioliscppclass extended with@staticmethod from_lat_az(lat_deg, muzzle_velocity_fps, az_deg)py_ballisticcalc.exts(base_types.pxd):BCLIBC_Atmospherecppclass extended with@staticmethod from_conditions(t_c, p_hpa, alt_ft, humidity)py_ballisticcalc.exts(bind.pyx): addedBCLIBC_Coriolis_from_lat_azandBCLIBC_Atmosphere_from_conditionsCython helpers (available for callers that hold Python domain objects directly;_from_pyobjectvariants retained for the same reason)- bclibc submodule bumped to
v1.1.2— addsBCLIBC_Shot/to_shot_props(),BCLIBC_Coriolis::from_lat_az(),BCLIBC_Atmosphere::from_conditions()(Steps 1–2); renames allBC*struct/enum types inbclibc_ffi.htoBCLIBCFFI_*and addsBCLIBCFFI_*_shot()entry points in the C FFI layer (Step 3a, breaking for C FFI consumers); no effect on Cython path (base_types.hppBCLIBC_*types are unchanged)
Fixed¶
_find_zero_angle()(base_engine.py): units mismatch in Ridder's bracket-convergence checks —cZeroFindingAccuracyis a height tolerance in feet but was also used as an angle tolerance in radians (if abs(next_angle - mid_angle) < cZeroFindingAccuracy); with the default5e-6the angle bracket had to narrow to5e-6 rad ≈ 0.0003°, causing oscillation and exhaustingcMaxIterationson certain high-elevation geometries; additionally, the Python implementation lacked explicit height-error checks (|f_mid|,|f_next|) that the C++ version has, so convergence was never declared even when the bullet already crossed the sight line within tolerance; fix: introduce a separateangle_tol = 1e-7 radfor bracket convergence, add|f_mid| < cZeroFindingAccuracyand|f_next| < cZeroFindingAccuracyheight checks, and evaluatef_nextbefore the angle-step exit — resolves #204- bclibc submodule bumped to
v1.1.3—src/engine.cpp,tiny_bclibc/include/tiny_bclibc/engine.h: same Ridder's-method fix —f_nextnow evaluated before theangle_tol/kRiddersAngleTolcheck in both the C++ and C implementations CythonizedBaseTrajData.__str__unpacked(name, value)from an iterator that yields scalar values — raisedValueErroron any callVacuum.__init__(conditions.py):Atmo.__init__usespressure or standard_pressure(...)so passingpressure=0(falsy) stores_p0 = 1013.25 hPainstead of0;Vacuum.__init__corrected_pressureand_density_ratiobut not_p0; any C++ wrapper reading_p0directly (e.g. Dart FFI, WASM) would receive 1013.25 hPa for vacuum; fix: explicitly setself._p0 = 0.0inVacuum.__init__aftersuper().__init__()BCLIBC_ShotProps_from_pyobject(bind.pyx):shot_info.windswas accessed twice (once forlen(), once for the for-loop);Shot.windsreturnstuple(self._winds)— a new tuple on every call; under free-threading a concurrent mutation of_windsbetween the two accesses could makewind_count > winds_vec.size(), causing an out-of-bounds read inBCLIBC_Shot::to_shot_props(); fix: cachewinds_py = shot_info.windsonce before both usesBCLIBC_ShotProps_from_pyobject(bind.pyx):shot_info.ammo,.ammo.dm,.atmo, and.weaponwere traversed multiple times each without being cached incdef objectlocals, violating the function's own documented invariant ("Cython may forget to add DECREF") — each chain traversal creates a temporary reference that Cython may not release on exception paths; fix: cache all intermediates in namedcdef objectlocals at the top of the function
Tests¶
TestIssue204added totests/test_issues.py— parametrized regression suite covering 9 high-elevation target points from issue #204 that previously raisedZeroFindingError; tests bothzero_angle()(iterative + Ridder's fallback) andfind_zero_angle()(direct Ridder's); all engines run withcStepMultiplier=5.0to keep runtime reasonable on high-elevation trajectories
CI¶
py_ballisticcalc.exts/pyproject.toml: removedenable = ["cpython-freethreading"]— no longer a valid enable group in cibuildwheel 4.x; removedcp313t-*frombuildselectors — Python 3.13t is no longer available in cibuildwheel 4.1.0 (left preview stage); final selectors:cp311-* cp314t-*test_full_matrixinpytest-cythonized-rk4-engine.ymlandpytest-cythonized-euler-engine.yml: changedfail-fastfromfalsetotrue— stops the 24-job matrix on the first failure instead of running all jobs to completioncibuildwheelbuild selectors:cp311-* cp313t-* cp314t-*;cp313t-*requiresenable = ["cpython-freethreading"]in cibuildwheel 3.4 (flag is deprecated butcp313tis silently skipped without it)- Cythonized engine test full matrix:
3.11, 3.14(abi3 boundary versions) +3.13t, 3.14t(distinct free-threaded ABIs);3.12,3.13removed — abi3 binary is identical across standard CPython versions pypi-publish.yml: extractedbuild-sdistjob — pure Python wheel and exts sdist built once onubuntu-latest; removed from per-OSbuildmatrix jobs to eliminate 5× redundant platform-independent builds
2.3.0b2 - 2026-05-13¶
Changed¶
BaseEngineConfigDictfields changed fromT | NonetoT—total=Falsealready provides optionality; explicitNonevalues no longer acceptedwith_no_minimum_velocityandwith_max_drop_zerodecorators typed withParamSpec,Concatenate, andfunctools.wraps— decorated methods now preserve their full signatures for static type checkersTrajectoryDataFilter.recordsclass-level mutable default= []removed; annotation-only declaration left, instance assigned in__init__isinstance(x, (A, B))replaced withisinstance(x, A | B)union syntax (PEP 604) acrossunit.py,vector.py,munition.py,trajectory_data.py,engines/scipy_engine.py- Implicit type aliases annotated with
TypeAliasininterface.py,munition.py,interpolation.py,trajectory_data.py typing.Selfmerged into existingfrom typing import (...)block inunit.py; duplicatefrom typing importblocks merged intrajectory_data.pyandgenerics/engine.pyimportlib.metadatacompatibility shim removed from_EngineLoader._get_entries_by_group—entry_points().select()used directly (available since Python 3.9)from __future__ import annotationsremoved fromengines/base_engine.py,visualize/plot.py, andshot.py;shot.ShotProps.from_shotreturn type updated toSelftyping.Selfmoved fromtyping_extensionsto stdlibtypingininterface.pytomllibimported directly from stdlib (Python 3.11+), version guard removedCallablemoved fromtypingtocollections.abcinhelpers.py
Removed¶
- Python 3.10 support EOL - removed all references to Python 3.10, updated CI and dependencies
CI¶
py_ballisticcalc.extswheels now target the Python stable ABI (cp311-abi3-*): one binary per platform/architecture is compatible with CPython 3.11 and all later standard releases; free-threaded Python 3.13t / 3.14t is built as separate version-specific wheelscibuildwheelreduced tocp311-* cp314t-*— eliminates redundant per-interpreter builds for standard CPython;cp313t-*dropped (deprecated in cibuildwheel 3.4, removed in next minor),cp314t-*requires noenableflag- Cythonized engine test full matrix reduced from 6 to 3 Python versions (
3.11, 3.14, 3.14t);3.12,3.13,3.13tremoved — abi3 binary is identical across standard versions, boundary versions provide sufficient coverage CIBW_ENVIRONMENT_PASS: SETUPTOOLS_SCM_PRETEND_VERSIONadded to the publish workflow — previously the version override was not forwarded intocibuildwheelbuild containers, causing+gHASHlocal version suffixes that PyPI rejectsuv auditpre-commit hook added — checks for known vulnerabilities in locked dependencies before each commituv lock --upgrade— all dev/docs dependencies updated; resolves 18 Dependabot security alerts (Pillow, urllib3, tornado, CairoSVG, fonttools, requests, virtualenv, Pygments, pymdown-extensions)pypi-publish.ymltrigger changed fromrelease: publishedtopush: tags: v*;create-releasejob added — generates and creates a draft GitHub Release automatically on tag push.github/actions/gen_release_notes/— new composite action with a Python script that builds formatted release notes fromCHANGELOG.md; supports optional> introand### Upgrade Notessections; auto-collects contributors viagit log.mailmapadded — normalizes git author aliases to GitHub usernames for consistent contributor attribution
2.2.10 - 2026-04-30¶
Changed¶
- bclibc C++ engine is now a git submodule (previously vendored sources)
- bump bclibc to v1.0.4
BCLIBC_Curve_fromPylistinpy_bind.cppnow delegates tobuild_pchip_curve_from_arrays— the universal PCHIP builder extracted intobase_types.hpp/base_types.cpp; eliminates code duplication between the Cython and FFI pathsBCLIBCFFI_CATCHmacro replaced withffi_call<F>template — same catch logic, no hiddenreturn, fullcatch(...)coverage for non-std exceptions across the FFI boundarytry_get_exactsignature changed fromvoid(exception-as-control-flow) tobool; call sites inget_atconverted fromtry/catchblocks to plainifchecks — removes overhead on the hot interpolation path
Fixed¶
- bclibc: undefined behavior in
find_zero_angle— missingreturnafter Ridder's loop - bclibc:
~BCLIBC_TrajectoryDataFilterdestructor no longer throws (wrapped intry/catch) - bclibc:
BCLIBC_BaseTrajData::operator[]bounds check used wrong constant and>instead of>= - bclibc:
BCLIBC_TrajectoryData::interpolatebounds check used>instead of>=, allowingFLAGkey - bclibc:
try_get_exactrefactored from exception-as-control-flow to returningbool - bclibc: memory leak in
BCLIBCFFI_integratewhentoCloop throws aftermalloc - bclibc: infinite loop guard added for
calc_step <= 0in Euler and RK4 integrators - bclibc:
BCLIBC_ShotPropsconstructor no longer propagatesdomain_errorfrom stability coefficient calculation - bclibc:
BCLIBCFFI_CATCHmacro replaced with type-safeffi_call<F>template (addscatch(...)for non-std exceptions)
2.2.9 - 2026-03-16¶
Changed¶
- Updated CI dependencies versions
- Updated a list of package dependencies in pyproject.toml
Fixed¶
- Fixed
Unit.Jouleconversion factor inEnergyclass returning incorrect foot-pounds to Joules values. - Types annotations codestyle fix
- Security fix for vulnerability described at (https://github.com/o-murphy/py-ballisticcalc/security/dependabot/1)
2.2.8 - 2026-01-26¶
Added¶
Calculatornow can be used as a context manager
Changed¶
Calculator- improved initialisation and type annotations- engines
__init__signature adjusted for consistency
Fixed¶
- Type annotations fix in
unit.py EngineProtocol- type annotations fixCalculator- type annotations fix- Type annotations modernized to Python 3.10+ style (PEP 604, PEP 585)
Optional[X]→X | NoneUnion[X, Y]→X | YList[X]→list[X],Dict[K, V]→dict[K, V],Tuple[X, Y]→tuple[X, Y]- Removed unused typing imports
- Updated .pyi stub files for Cython extensions
- Added type guards in
uconv.py
2.2.7 - 2025-12-26¶
Changed¶
- C++ headers includes refactoring
- Redundant Null Pointer Check Removal in
BCLIBC_Coriolis - Better
BCLIBC_WindSockinitialization in Cython/C++ BCLIBC_WindSock_from_pylistrenamed toBCLIBC_WindSock_from_pytuple- C++ to Python Exception bridge improved, avoids multiple rethrows, uses
dynamic_cast - C++ to Python Exception bridge
many_exception_handlerrenamed toexception_dispatch BCLIBC_BaseTrajData::get_key_valreplaced withBCLIBC_BaseTrajData::operator[]BCLIBC_BaseTrajSeq::get_key_valreplaced withBCLIBC_BaseTrajSeq::operator[]BCLIBC_TrajectoryData::get_key_valreplaced withBCLIBC_TrajectoryData::operator[]
Fix¶
- Docstrings fix according to Issue #299
2.2.6.post1 - 2025-12-14¶
Changed¶
- Removed unnecessary atmosphere precalculation in C++ rk4 integrator
- Removed unnecessary 'edited' trigger from pypi-publish.yml
2.2.6 – 2025-12-13¶
Changed¶
- Optimized C++ based RK4 integrator performance
- V3dT uses square check instead of sqrt for magnitude calculation for performance
Fixed¶
- C++ based TrajectoryDataFilter initialization fix
Compatibility¶
- Fully compatible with v2.2.3 (no breaking changes).
- Rebuilding wheels is recommended for distributors.
2.2.5 – 2025-12-11¶
Added¶
- Improvements in integrator termination control and integration helpers.
- CI/CD enhancements including reusable wheel builds and better version extraction.
Changed¶
- Major internal migration to a C++-centric engine design.
- Simplified Cython bindings and improved Python-level ergonomics.
- Significant memory usage optimizations.
- Thread-safety improvements in core engine components.
- Updated documentation, README, and contributor guidelines.
Fixed¶
- Eliminated redundant copying in internal data pipelines.
- Fixed documentation build issues.
- Numerous bug fixes across the engine, wrappers, and integration logic.
Compatibility¶
- Fully compatible with v2.2.3 (no breaking changes).
- Rebuilding wheels is recommended for distributors.
- Thread-safety improvements may positively affect multi-threaded usage.
2.2.5rc3 - 2025-12-10¶
Changed¶
- Refactoring and features improvements
- Attempted to use std::function for better functionality
Fixed¶
- Made type annotations better and more accurate
2.2.5rc2 - 2025-12-02¶
Added¶
- C++-level
GenericTerminatorfor better control CythonizedBaseIntegrationEngine.integrate_raw_atmethod to integrate by key_attribute and target_value- Thread safety with
std::recursive_mutexforBCLIBC_Enginefields
Changed¶
- Control integrator termination from external handlers
- Much more safe
BCLIBC_EssentialTerminatorsusage - Refactored termination control from outer
BCLIBC_EssentialTerminators
Fixed¶
- C++ memory usage optimization
2.2.5rc1 - 2025-11-28¶
Added¶
- Automatic version detection during build/install using
setuptools-scm - RAII initialization to prevent memory leaks in trajectory filters
- Dense output support for trajectory handling
Changed¶
- Major refactor: Fully ported ballistic engine from C/Cython to C++ with thin Cython wrappers
- Cython now provides only a thin interface with most computations in native C++
BaseTrajSeq,TrajectoryDataFilter,V3dT,BCLIBC_Coriolis,BCLIBC_Wind, andBCLIBC_ShotPropsrefactored as C++ classes/enum classes- Optimized solvers (rk4, euler) and trajectory computations
- Enhanced trajectory data filtering and interpolation
- Unified exception handling in apex, range, and zero-finding calculations
Fixed¶
- Memory management across engine and trajectory structures
- Pickling support for
Calculatorinterface - Log level issues
- TDF caching and timestep handling
- Numerical stability including look angles near 90°
Improved¶
- Performance through optimized internal data structures using
std::vector - Reduced complexity and duplication in Cython wrappers
- Partial compile-time logging disabling to reduce overhead
- Zero-finding and engine computations robustness
2.2.5b2 - 2025-11-27¶
Fixed¶
interface::Calculatorcustom serialization methods for pickling
2.2.5b1 - 2025-11-11¶
Added¶
- C++ TrajectoryDataFilter implementation
Changed¶
- Ported TDF to C++
- Refactored C++ Engine wrapper
- Wrapped
Engine.release_trajectoryin C++
Fixed¶
- macOS builds compatibility
- TDF exceptions handling
CythonizedBaseIntegrationEngine.integrateerrors check- Removed useless arguments from integratefunc
2.2.4rc1 - 2025-11-09¶
Added¶
- Namespace organization improvements
- Helper functions for better code organization
Changed¶
- Multiple refactoring passes (Refactor2C_4, Refactor2C_5, Refactor2C_6)
- C-level optimizations for better performance
- Cythonized and C sources updates
Fixed¶
- Log level configuration
- Setup process
- Conftest configuration
- TDF cache interpolation for efficiency
- TDF time step check
- Annotations in TDF
BCLIBC_Coriolis_adjustRangefunction- Memory management with memset
2.2.3 - 2025-10-19¶
Added¶
- Python 3.14 and 3.14t support
- ARM wheel building support
- Manual pytest workflow runner for GitHub Actions
- Thread safety improvements for
interface.Calculator
Changed¶
- Dropped support for Python 3.9
- Updated CI workflows to include Python 3.14
- Refactored
WindSock_tto use built-in structure - Reduced
.ipynbdependency footprint for VS Code and PyCharm - Updated reusable pytest workflows with exit code checks
Fixed¶
- Dependency issues for new Python versions
Coriolis_t_from_pyobjecthandlingShotProps_t_freeResourcesto safely NULLIFY internal pointers.pylintrcissues- Dependabot configuration
Removed¶
- Deprecated runners (macos-13)
- Deprecated workflows (cibuildwheel_test.yml)
- Deprecated
interface.Calculator.cdmproperty
2.2.2 - 2025-10-16¶
Added¶
- Valgrind-based workflows for leak detection
- New benchmarking details in documentation
CBaseTrajSeq_t_lenhelper function- Enhanced test helpers and internal data structure creation utilities
Changed¶
- Moved integration routines (
_integrate_rk4,_integrate_euler) fully to C CBaseTrajSeqnow wraps pure-CCBaseTrajSeq_t- Replaced Python-level calls with direct C invocations
- Improved
BaseTrajDataTstructure and method access
Fixed¶
- Multiple memory leaks in trajectory and curve management
- Enhanced C++ compatibility with
extern "C"declarations - Typos and mkdocs build issues
termination_reasonchecks in C integrators
Removed¶
- Redundant pointer dereferences and casts
- Redundant imports and legacy declarations
2.2.1 - 2025-10-08¶
Added¶
- Import guards for optional dependencies (pandas, numpy, scipy)
- Version-matching guards in
exts
Changed¶
- Updated hooks and contribution guides
- Refreshed code examples for clarity
Fixed¶
- Engine loading to prevent import errors when scientific libraries are missing
- Mypy and ruff checks now pass cleanly
Removed¶
- Redundant include guard from
bind.c
2.2.0 - 2025-10-03¶
Added¶
- New RK4 (Runge-Kutta 4) integrator
- Coriolis effect support in trajectory calculations
- Python 3.13 support
- Entry points for engines
- Override for
getCalcStepin RK4 engine
Changed¶
- Set RK4 engine as default
- Refactored integration with SciPy
- General Cythonization of critical paths
- Some components rewritten directly in C
- Standardized slant terminology
- Restructured and prettified documentation
Fixed¶
- RK4 engine implementation
- Type annotations and configuration
- Tests and logger cleanup
Improved¶
- Zeroing features and refinements with Cython implementation
- Documentation expanded with user guides and explanations
2.2.0rc2 - 2025-09-25¶
Changed¶
- Extended documentation
Fixed¶
- Various issues from v2.2.0rc1
2.2.0rc1 - 2025-09-15¶
Added¶
- Entry points for engines
- RK4 integrator with passing tests
- New tests for zeroing
- Coriolis effect implementation
Changed¶
- Refactored
trajectory_calc.py - Cythonized engines refactoring
- Inline vector operations optimization
- Restructured and prettified documentation
- Updated README
Fixed¶
- Test fixes and logger cleanup
- Various SciPy engine configuration issues
2.2.0b7 - 2025-08-26¶
Added¶
- Piecewise Cubic Hermite Interpolation (PCHIP) to trajectory data
Optional[RangeError]toHitResult- Implemented new
dense_outputflag - Cython-specific unit tests
- Enhanced documentation with standardized docstrings
Changed¶
- RK4 engines run with larger
DEFAULT_STEPfor faster results - Replaced
EngineProtocol.trajectory()with.integrate() - Rewrote
TrajectoryDataFilterto interpolate for all requested points - Rewrote chunks of Cython for better test compatibility
- Renamed
density_factortodensity_ratio - Renamed
PreferredUnits.defaults()to.restore_defaults()
Deprecated¶
helpers.py::must_fire()- useCalculator.fire(raise_range_error=True)insteadextra_dataflag
Fixed¶
- Multiple issues including #199, #202, #203, #209, #211, and #35
Removed¶
- Dev dependencies from standard install
2.2.0b6 - 2025-08-09¶
Added¶
- ZeroStudy.ipynb with zeroing/fire-solution performance study
- BenchmarkEngines.md analysis
- Enhanced zeroing features
Changed¶
- RK4 engine set as default after fixes
- Standardized slant terminology in README
- Cythonized engines refactoring
- Inline vector operations
Fixed¶
- RK4 engine implementation
- mkdocs build
Improved¶
- All engines now support
.find_max_range()for any angle
2.2.0b5 - 2025-07-04¶
Changed¶
- Type annotations updates
- Ruff CI setup
Fixed¶
- SciPy engine annotations
- SciPy configuration issues
2.2.0b4 - 2025-07-01¶
Added¶
- RK4
getCalcStepoverride
Changed¶
- SciPy integration improvements
2.2.0b3.post1 - 2025-06-25¶
Changed¶
- SciPy integration refinements
2.2.0b3 - 2025-06-25¶
Changed¶
- SciPy integration improvements
2.2.0b2 - 2025-06-20¶
Changed¶
- SciPy integration enhancements
2.2.0b1 - 2025-06-13¶
Added¶
- RK4 integrator implementation
Fixed¶
- Test suite improvements
- InterfaceConfigDict usages
2.1.1b3 - 2025-06-10¶
Added¶
- Python 3.13 support
2.1.1b2 - 2025-06-04¶
Changed¶
- Refactored
trajectory_calc.py
2.1.1b1 - 2025-06-03¶
Added¶
- Entry points for engines
2.1.0 - 2025-05-22¶
Added¶
- Vacuum atmosphere implementation
- Helper utilities and test helpers
add_time_of_flight_axismethod- Enhanced documentation with docstrings
- Automatic CI with UV for better performance
Changed¶
- Switched to powder temperature and sensitivity options
- Boostrap Cython modules
- Refactored munition cythonization
- Refactored wind_vector
- Configuration bind refactoring for cythonic TrajectoryCalc
Fixed¶
- Atmospheric model issues
- Vector performance (changed to NamedTuple)
- Velocity for temperature calculations
- Exception handling improvements
- Incomplete shot handling
- Trajectories that bend backwards
_init_trajectoryissues- Issue #130
Improved¶
- Performance optimizations with high-optimized Euler
- CI pipelines with UV
- Type annotations with Mypy
2.1.0rc2 - 2025-04-27¶
Fixed¶
- Partial fix to issue #155
2.1.0rc1 - 2025-04-13¶
Fixed¶
- Completed implementation for issue #164
2.1.0b7 - 2025-03-25¶
Added¶
- Vacuum Atmo implementation
- Unit test for issue #160
Changed¶
- Vector changed to NamedTuple for better performance
- Switched CI to UV for performance boost
Fixed¶
- All atmospheric model issues from #157
- Simple fix to #160
- CI pipelines
- Mypy typing issues
2.1.0b6 - 2025-02-18¶
Changed¶
- Various improvements and refinements
2.1.0b5 - 2025-02-06¶
Added¶
- Incomplete shot handling
2.1.0b4 - 2025-01-25¶
Added¶
- Helper utilities and test helpers
add_time_of_flight_axismethod- Trusted publishers support
Changed¶
- Configuration bind refactoring for cythonic TrajectoryCalc
Fixed¶
- Issue #141
- Trajectories that bend backwards