Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
.git
.gitignore
.env*

.venv/
venv/
env/
**/.venv/
**/venv/
**/node_modules/

**/__pycache__/
*.py[cod]
.pytest_cache/
.ruff_cache/
.coverage
coverage.xml
junit*.xml
htmlcov/
test-results/
tests/

data/*.db
*.sqlite
*.sqlite3
.codex-*

secrets/
**/secrets/
*.pem
*.key
*.p12
*.pfx

output/builds/build/
29 changes: 18 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,17 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install ruff
pip install pytest ruff

- name: Lint with ruff
- name: Lint repository for fatal errors
run: |
ruff check --select=E,F,W --ignore=E501,E402 \
main.py \
end_to_end_test.py \
output/builds/src/science/ \
output/builds/src/orbit_propagator.py \
output/builds/src/conjunction_screener.py \
output/builds/src/multi_sensor_fusion.py \
output/builds/src/observation_scheduler.py
ruff check . --select=E9,F63,F7,F82 \
--exclude=output/builds/src/hardware/ascom_hal.py

- name: Run test suite
env:
PYTHONIOENCODING: utf-8
run: pytest -q

- name: Run end-to-end pipeline test
env:
Expand Down Expand Up @@ -68,4 +67,12 @@ jobs:
- name: Verify CMakeLists.txt parses
run: |
cd output/builds/src
cmake -B build -DCMAKE_CUDA_ARCHITECTURES=87 2>&1 || echo "CMake parse check complete (CUDA toolkit not installed — expected)"
if cmake -B build -DCMAKE_CUDA_ARCHITECTURES=87 >cmake.log 2>&1; then
cat cmake.log
elif grep -Eq "Failed to find nvcc|Could not find nvcc|No CMAKE_CUDA_COMPILER" cmake.log; then
cat cmake.log
echo "CMake parse check complete (CUDA toolkit not installed — expected)"
else
cat cmake.log
exit 1
fi
95 changes: 3 additions & 92 deletions output/builds/src/photometry.cpp
Original file line number Diff line number Diff line change
@@ -1,92 +1,3 @@
/**
* photometry.cpp - SentinelForge Edge Pipeline
* PSF fitting photometry and magnitude measurement.
* Equation: I(x,y) = A * exp(-((x-x0)^2/(2*sx^2) + (y-y0)^2/(2*sy^2))) + B
* Generated by Agent 3 (Plate Solver)
*/

#include <cstdio>
#include <cmath>
#include <vector>

constexpr double FWHM_DEFAULT = 2.5;
constexpr int APERTURE_RADII[] = {3, 5, 8}; // pixels
constexpr double ZERO_POINT_MAG = 25.0; // Instrumental zero point

struct PhotometryResult {
double x, y; // Sub-pixel centroid
double flux_aper[3]; // Aperture photometry at 3 radii
double flux_psf; // PSF-fitted flux
double mag_inst; // Instrumental magnitude
double mag_err; // Magnitude uncertainty
double snr;
double fwhm_measured;
double background_local;
bool saturated;
};

class Photometer {
public:
/**
* Aperture photometry: sum flux in circular aperture,
* subtract background from annulus.
*/
static PhotometryResult measure(const float* frame, int width, int height,
double cx, double cy) {
PhotometryResult r = {};
r.x = cx;
r.y = cy;
r.saturated = false;

// Background from annulus (r=10 to r=15 pixels)
double bg_sum = 0;
int bg_count = 0;
for (int dy = -15; dy <= 15; dy++) {
for (int dx = -15; dx <= 15; dx++) {
double dist = sqrt(dx*dx + dy*dy);
if (dist < 10 || dist > 15) continue;
int px = (int)cx + dx, py = (int)cy + dy;
if (px < 0 || px >= width || py < 0 || py >= height) continue;
bg_sum += frame[py * width + px];
bg_count++;
}
}
r.background_local = (bg_count > 0) ? bg_sum / bg_count : 0;

// Aperture photometry at multiple radii
for (int ai = 0; ai < 3; ai++) {
int aper = APERTURE_RADII[ai];
double flux = 0;
int n_pix = 0;
for (int dy = -aper; dy <= aper; dy++) {
for (int dx = -aper; dx <= aper; dx++) {
if (dx*dx + dy*dy > aper*aper) continue;
int px = (int)cx + dx, py = (int)cy + dy;
if (px < 0 || px >= width || py < 0 || py >= height) continue;
double val = frame[py * width + px] - r.background_local;
flux += val;
n_pix++;
if (frame[py * width + px] >= 65535) r.saturated = true;
}
}
r.flux_aper[ai] = flux;
}

// Use middle aperture for magnitude
r.flux_psf = r.flux_aper[1];
if (r.flux_psf > 0) {
r.mag_inst = ZERO_POINT_MAG - 2.5 * log10(r.flux_psf);
double noise = sqrt(r.flux_psf + M_PI * 25 *
(r.background_local + 10*10));
r.snr = r.flux_psf / noise;
r.mag_err = 1.0857 / r.snr; // Pogson error
} else {
r.mag_inst = 99.9;
r.snr = 0;
r.mag_err = 99.9;
}

r.fwhm_measured = FWHM_DEFAULT;
return r;
}
};
// Compatibility path for legacy builds. CMake's canonical implementation lives
// in src/cpp, and including it here keeps both build paths on the same source.
#include "cpp/photometry.cpp"
25 changes: 25 additions & 0 deletions tests/test_build_hygiene.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]


def test_legacy_photometry_source_forwards_to_canonical_implementation():
forwarder = (ROOT / "output/builds/src/photometry.cpp").read_text(encoding="utf-8")

assert '#include "cpp/photometry.cpp"' in forwarder
assert "class Photometer" not in forwarder


def test_dockerignore_excludes_sensitive_and_generated_content():
patterns = set((ROOT / ".dockerignore").read_text(encoding="utf-8").splitlines())

assert {".git", ".env*", "**/node_modules/", "**/__pycache__/", "tests/", "secrets/"} <= patterns


def test_ci_runs_tests_and_does_not_swallow_unknown_cmake_failures():
workflow = (ROOT / ".github/workflows/ci.yml").read_text(encoding="utf-8")

assert "ruff check ." in workflow
assert "run: pytest -q" in workflow
assert "2>&1 || echo" not in workflow
33 changes: 21 additions & 12 deletions tests/test_science_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,30 @@ def test_import(self):

def test_kepler_period(self):
"""Verify Kepler's third law: T² ∝ a³"""
mu = 398600.4418 # km³/s²
a = 6778 # km (LEO ~407 km alt)
T = 2 * np.pi * np.sqrt(a**3 / mu)
assert 5400 < T < 5700, f"LEO period should be ~5554s, got {T:.0f}s"
from orbit_propagator import OrbitalElements

orbit = OrbitalElements(
norad_id=25544, epoch=0.0, a=6_778_000.0, e=0.0001,
i=np.radians(51.6), raan=0.0, argp=0.0, M=0.0,
)
assert 5400 < orbit.period_sec < 5700

def test_j2_perturbation_nonzero(self):
"""J2 RAAN regression should be non-zero for inclined orbits."""
J2 = 1.08263e-3
Re = 6371.0
a = 7000.0
e = 0.001
inc = np.radians(51.6) # ISS inclination
n = np.sqrt(398600.4418 / a**3)
raan_dot = -1.5 * n * J2 * (Re / a)**2 * np.cos(inc) / (1 - e**2)**2
assert abs(raan_dot) > 1e-8, "J2 RAAN regression should be nonzero"
from orbit_propagator import OrbitalElements, OrbitPropagator

orbit = OrbitalElements(
norad_id=25544, epoch=0.0, a=7_000_000.0, e=0.001,
i=np.radians(51.6), raan=0.5, argp=0.0, M=0.0,
)
propagated_elements = []
propagator = OrbitPropagator()
to_state = propagator.kepler_to_state
propagator.kepler_to_state = lambda elem: (propagated_elements.append(elem), to_state(elem))[1]

propagator.propagate(orbit, 600.0)

assert propagated_elements[0].raan != orbit.raan


class TestConjunctionScreener:
Expand Down
Loading