Skip to content

Commit cc23412

Browse files
Dr. Troy Dean RockwoodDr. Troy Dean Rockwood
authored andcommitted
Prepare Python port for publication
1 parent ce40fd9 commit cc23412

10 files changed

Lines changed: 347 additions & 24 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
pull_request:
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
parity:
13+
name: Python ${{ matrix.python-version }} parity
14+
runs-on: ubuntu-latest
15+
strategy:
16+
fail-fast: false
17+
matrix:
18+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
19+
defaults:
20+
run:
21+
working-directory: distlink-python
22+
steps:
23+
- name: Check out Python port
24+
uses: actions/checkout@v7
25+
with:
26+
path: distlink-python
27+
- name: Check out pinned C++20 oracle
28+
uses: actions/checkout@v7
29+
with:
30+
repository: troyrock/distlink-cpp
31+
ref: 58c1c29ad1555920e004ff6fc8b932e954751147
32+
path: distlink-cpp
33+
- name: Set up Python
34+
uses: actions/setup-python@v7
35+
with:
36+
python-version: ${{ matrix.python-version }}
37+
- name: Run C++ oracle parity suite
38+
run: python -m unittest discover -s tests -v
39+
40+
package:
41+
name: Build and inspect distributions
42+
runs-on: ubuntu-latest
43+
steps:
44+
- uses: actions/checkout@v7
45+
- uses: actions/setup-python@v7
46+
with:
47+
python-version: "3.14"
48+
- name: Install build tools
49+
run: python -m pip install --upgrade build twine
50+
- name: Build wheel and source distribution
51+
run: python -m build
52+
- name: Inspect package metadata
53+
run: python -m twine check --strict dist/*
54+
- name: Install and smoke-test the wheel
55+
run: |
56+
python -m pip install --force-reinstall dist/*.whl
57+
cd "${RUNNER_TEMP}"
58+
python -c "from distlink import COrbitData; print(COrbitData())"
59+
- name: Preserve distribution artifacts
60+
uses: actions/upload-artifact@v7
61+
with:
62+
name: python-package-distributions
63+
path: dist/
64+
if-no-files-found: error

‎.github/workflows/release.yml‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Publish to PyPI
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
permissions:
8+
contents: read
9+
10+
jobs:
11+
build:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v7
15+
- uses: actions/setup-python@v7
16+
with:
17+
python-version: "3.14"
18+
- name: Verify release tag matches package version
19+
run: >-
20+
python -c "import os, pathlib, tomllib;
21+
version = tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'];
22+
tag = os.environ['GITHUB_REF_NAME'];
23+
assert tag == f'v{version}', f'release tag {tag!r} does not match v{version}'"
24+
- name: Build distributions
25+
run: |
26+
python -m pip install --upgrade build twine
27+
python -m build
28+
python -m twine check --strict dist/*
29+
- uses: actions/upload-artifact@v7
30+
with:
31+
name: python-package-distributions
32+
path: dist/
33+
if-no-files-found: error
34+
35+
publish:
36+
needs: build
37+
runs-on: ubuntu-latest
38+
environment:
39+
name: pypi
40+
url: https://pypi.org/p/distlink-python
41+
permissions:
42+
id-token: write
43+
steps:
44+
- uses: actions/download-artifact@v8
45+
with:
46+
name: python-package-distributions
47+
path: dist/
48+
- name: Publish to PyPI
49+
uses: pypa/gh-action-pypi-publish@release/v1

‎.github/workflows/test-publish.yml‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Publish to TestPyPI
2+
3+
on:
4+
workflow_dispatch:
5+
6+
permissions:
7+
contents: read
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v7
14+
- uses: actions/setup-python@v7
15+
with:
16+
python-version: "3.14"
17+
- name: Build distributions
18+
run: |
19+
python -m pip install --upgrade build twine
20+
python -m build
21+
python -m twine check --strict dist/*
22+
- uses: actions/upload-artifact@v7
23+
with:
24+
name: python-package-distributions
25+
path: dist/
26+
if-no-files-found: error
27+
28+
publish:
29+
needs: build
30+
runs-on: ubuntu-latest
31+
environment:
32+
name: testpypi
33+
url: https://test.pypi.org/p/distlink-python
34+
permissions:
35+
id-token: write
36+
steps:
37+
- uses: actions/download-artifact@v8
38+
with:
39+
name: python-package-distributions
40+
path: dist/
41+
- name: Publish to TestPyPI
42+
uses: pypa/gh-action-pypi-publish@release/v1
43+
with:
44+
repository-url: https://test.pypi.org/legacy/

‎CHANGELOG.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Changelog
2+
3+
All notable changes to the Python port are recorded here.
4+
5+
## 0.2.0 - 2026-09-07
6+
7+
- Match the resolved C++20 oracle behavior for circular and coplanar orbits.
8+
- Make the polynomial root search deterministic across calls and languages.
9+
- Validate orbital elements and numerical options consistently.
10+
- Initialize all public result fields and use monotonic elapsed timing.
11+
- Add comprehensive C++ oracle parity and edge-case coverage.
12+
- Add release metadata, attribution, CI, and Trusted Publishing workflows.
13+
14+
## 0.1.0 - 2026-09-04
15+
16+
- Complete the initial pure-Python source translation.
17+
- Add elliptic, hyperbolic, direct-search, and linking-coefficient parity tests.

‎LICENSE‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
MIT License
2+
3+
Copyright (c) 2018-2020 R.V. Baluev and D.V. Mikryukov
4+
Copyright (c) 2026 Troy D. Rockwood (Python translation and modifications)
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE.

‎MANIFEST.in‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
include CHANGELOG.md
2+
include PUBLISHING.md
3+
include example.py
4+
recursive-include tests *.cpp *.py

‎PUBLISHING.md‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Publishing distlink-python
2+
3+
The repository contains separate workflows for validation, TestPyPI, and the
4+
production PyPI index. Long-lived PyPI tokens are not required.
5+
6+
## One-time setup
7+
8+
1. Create the public GitHub repository `troyrock/distlink-python` and push the
9+
`main` branch.
10+
2. On TestPyPI, create a pending Trusted Publisher with:
11+
- owner: `troyrock`
12+
- repository: `distlink-python`
13+
- workflow: `test-publish.yml`
14+
- environment: `testpypi`
15+
3. On PyPI, create a pending Trusted Publisher with:
16+
- owner: `troyrock`
17+
- repository: `distlink-python`
18+
- workflow: `release.yml`
19+
- environment: `pypi`
20+
4. Create matching `testpypi` and `pypi` GitHub environments. Configure
21+
required reviewers for the production `pypi` environment if desired.
22+
23+
The package name is not reserved until the first successful upload. Confirm
24+
that `distlink-python` is still available immediately before publishing.
25+
26+
## TestPyPI release
27+
28+
1. Confirm that the CI workflow passes on `main`.
29+
2. Run the **Publish to TestPyPI** workflow manually.
30+
3. Install the uploaded version in a clean environment and run a smoke test:
31+
32+
```text
33+
python -m pip install --index-url https://test.pypi.org/simple/ --no-deps distlink-python
34+
python -c "from distlink import COrbitData; print(COrbitData())"
35+
```
36+
37+
## Production release
38+
39+
1. Update the version in `pyproject.toml` and add its release notes to
40+
`CHANGELOG.md`. PyPI does not permit replacing an uploaded distribution
41+
file with different content.
42+
2. Confirm that CI and the TestPyPI installation pass.
43+
3. Create and publish a GitHub release whose tag matches the version, for
44+
example `v0.2.0`.
45+
4. Approve the `pypi` environment deployment if an approval rule is enabled.
46+
Publishing the GitHub release triggers `.github/workflows/release.yml`.

‎README.md‎

Lines changed: 73 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,32 @@
11
# distlink-python
22

3-
A source-level Python port of the `distlink-cpp` implementation of the
4-
two-orbit minimum orbit intersection distance (MOID) and linking coefficients.
3+
An unofficial, source-level Python port of the DISTLINK C++ library for
4+
computing the minimum orbit intersection distance (MOID) and orbital linking
5+
coefficients. The public API retains the C++ names to make examples and
6+
results straightforward to compare.
57

6-
The public API intentionally retains the C++ names so existing examples are
7-
easy to translate:
8+
This port is maintained independently and is not represented as an official
9+
release by the original DISTLINK authors.
10+
11+
## Installation
12+
13+
After the first PyPI release:
14+
15+
```text
16+
python -m pip install distlink-python
17+
```
18+
19+
The distribution is named `distlink-python`; the import name is `distlink`.
20+
To install the current development version directly from GitHub:
21+
22+
```text
23+
python -m pip install git+https://github.com/troyrock/distlink-python.git
24+
```
25+
26+
The implementation uses only the Python standard library and supports Python
27+
3.10 and later.
28+
29+
## Quick start
830

931
```python
1032
from distlink import COrbitData, MOID_fast, detect_suitable_options
@@ -16,30 +38,60 @@ result = MOID_fast(earth, asteroid, max_root_error, min_root_error)
1638
print(result.distance, result.good)
1739
```
1840

19-
Angles are in radians. Distance results use the same unit as the supplied
20-
semimajor axes. The implementation uses only the Python standard library.
21-
22-
Run the standard-library test suite from this directory with:
41+
Angles are in radians. Distance results use the same unit as the supplied
42+
semimajor axes. Python `float` corresponds to the C++ `double`
43+
instantiation.
2344

24-
```text
25-
python -m unittest discover -s tests -v
26-
```
45+
## Input and numerical behavior
2746

28-
`tests/oracle.cpp` is a small command-line adapter around the unmodified C++
29-
library. The parity tests compile it as C++20 with GNU C++ and compare Python
30-
results against fresh C++ results.
31-
32-
Orbital elements and numerical options must be finite. Eccentricity must be
33-
nonnegative and exactly parabolic orbits (`e == 1`) are rejected. A nonzero
47+
Orbital elements and numerical options must be finite. Eccentricity must be
48+
nonnegative, and exactly parabolic orbits (`e == 1`) are rejected. A nonzero
3449
semimajor axis is canonicalized to positive for an ellipse and negative for a
35-
hyperbola. Invalid inputs raise `ValueError`.
50+
hyperbola. Invalid inputs raise `ValueError`.
3651

37-
`MOID_fast` is deterministic and supports circular first orbits directly: it
52+
`MOID_fast` is deterministic and supports circular first orbits directly. It
3853
automatically exchanges the pair when only the first orbit is circular and
39-
uses the exact two-circle solution when both are circular. For other results
54+
uses the exact two-circle solution when both are circular. For other results
4055
marked unreliable, retrying in the opposite order remains reasonable before
4156
using `MOID_direct_search` as the fallback.
4257

43-
The direct scanner uses a full anomaly range for coplanar elliptic orbits. It
58+
The direct scanner uses a full anomaly range for coplanar elliptic orbits. It
4459
rejects a coplanar hyperbolic pair because no finite scan interval can in
4560
general bound both unbounded branches; use `MOID_fast` for that case.
61+
62+
## Verification against C++
63+
64+
The standard-library test suite compiles a fresh C++20 command-line oracle and
65+
compares Python results with it:
66+
67+
```text
68+
python -m unittest discover -s tests -v
69+
```
70+
71+
The release CI pins the oracle to
72+
[`troyrock/distlink-cpp@58c1c29`](https://github.com/troyrock/distlink-cpp/commit/58c1c29ad1555920e004ff6fc8b932e954751147).
73+
For a local checkout, place `distlink-python` and `distlink-cpp` beside one
74+
another, as they are in the development workspace.
75+
76+
## Provenance and references
77+
78+
This code is a direct Python translation of DISTLINK by Roman V. Baluev and
79+
Denis V. Mikryukov. The original project is distributed at
80+
[SourceForge](https://sourceforge.net/projects/distlink/); the maintained
81+
C++20 oracle used for this port is available at
82+
[`troyrock/distlink-cpp`](https://github.com/troyrock/distlink-cpp).
83+
84+
The underlying algorithms are described in:
85+
86+
- R. V. Baluev and D. V. Mikryukov, “Fast error-controlling MOID computation
87+
for confocal elliptic orbits,” *Astronomy and Computing* 27 (2019), 11–22,
88+
[doi:10.1016/j.ascom.2019.02.005](https://doi.org/10.1016/j.ascom.2019.02.005).
89+
- R. V. Baluev, “Fast error-safe MOID computation involving hyperbolic
90+
orbits,” *Astronomy and Computing* 34 (2021), 100440,
91+
[doi:10.1016/j.ascom.2020.100440](https://doi.org/10.1016/j.ascom.2020.100440).
92+
93+
## License
94+
95+
The original C++ work and this Python translation are distributed under the
96+
MIT License. The original authors' copyright and permission notice are
97+
preserved in [`LICENSE`](LICENSE) and in the translated source.

‎distlink.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
distributed under the MIT license reproduced below.
66
77
Copyright (c) 2018-2020 R.V. Baluev and D.V. Mikryukov
8+
Copyright (c) 2026 Troy D. Rockwood (Python translation and modifications)
89
910
Permission is hereby granted, free of charge, to any person obtaining a copy
1011
of this software and associated documentation files (the "Software"), to deal

0 commit comments

Comments
 (0)