Files
Microtracking/test_analyze_2026_11_04.py

168 lines
9.4 KiB
Python

"""Focused numerical and source-integrity tests for the 4 November analysis."""
import unittest
from unittest.mock import patch
import numpy as np
import pandas as pd
import analyze_2026_11_04 as analysis
class NovemberFourthAnalysisTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
raw, cls.telemetry, cls.events, cls.source_hash = analysis.parse_log()
cls.frame = analysis.calibrate(raw)
def test_signed_24_bit_conversion_and_calibration(self):
self.assertEqual(analysis.signed24(0xFFFFFF), -1)
self.assertEqual(analysis.signed24(0x800000), -8_388_608)
self.assertEqual(analysis.signed24(0x7FFFFF), 8_388_607)
row = self.frame.iloc[0]
expected_current = row.c_signed / analysis.ADC_MAX * 2.048 / (20 * .130)
expected_voltage = row.v_signed / analysis.ADC_MAX * 2.048 * 2
expected_irradiance = row.p_signed / analysis.ADC_MAX * 2048 / .2
self.assertAlmostEqual(row.isc_A, expected_current, places=14)
self.assertAlmostEqual(row.voc_V, expected_voltage, places=14)
self.assertAlmostEqual(row.irradiance_W_m2, expected_irradiance, places=12)
def test_source_counts_quality_and_gap_detection(self):
self.assertEqual(len(self.frame), 5049)
self.assertEqual(self.frame.source_line.iloc[0], 3)
self.assertEqual(int(self.frame.adc_clipped.sum()), 0)
self.assertEqual(int(self.frame.near_current_rail.sum()), 0)
self.assertEqual(int(self.frame.invalid_polarity.sum()), 0)
self.assertTrue(self.frame.illuminated.all())
self.assertEqual(int(self.frame.uptime_s.diff().lt(0).sum()), 0)
dt = self.frame.timestamp.diff().dt.total_seconds()
gaps = self.frame.loc[dt > 15, "timestamp"].dt.strftime("%H:%M:%S").tolist()
self.assertEqual(gaps, ["09:53:51", "09:57:17", "10:34:59", "16:04:23", "16:05:01"])
self.assertEqual(self.frame.attrs["firmware_versions"], ["1.1.3-ws"])
def test_no_integration_across_large_gap(self):
f = self.frame.iloc[:].copy()
edges = analysis.accepted_edges(f)
dt = f.timestamp.diff().dt.total_seconds()
self.assertTrue((dt[edges] <= 15).all())
self.assertEqual(int(edges.sum()), 5043)
result = analysis.metrics(f)
self.assertAlmostEqual(result["integrated_support_h"], 6.121388888888889, places=12)
self.assertAlmostEqual(result["estimated_mpp_energy_Wh"], 3.746297226684865, places=10)
self.assertAlmostEqual(result["irradiation_Wh_m2"], 6408.493782208228, places=8)
self.assertAlmostEqual(result["energy_weighted_aperture_efficiency_pct"], 23.411418108383202, places=9)
self.assertAlmostEqual(result["aperture_efficiency_at_peak_power_pct"], 33.21021299703548, places=9)
def test_uptime_reset_and_rejected_endpoint_break_support(self):
base = self.frame.iloc[:4].copy().reset_index(drop=True)
base.loc[2, "uptime_s"] = base.loc[1, "uptime_s"] - 1
edges = analysis.accepted_edges(base)
self.assertFalse(edges[2])
base.loc[3, "accepted"] = False
edges = analysis.accepted_edges(base)
self.assertFalse(edges[3])
def test_trapezoid_and_window_selection(self):
f = self.frame.iloc[:3].copy().reset_index(drop=True)
f["timestamp"] = pd.date_range("2026-11-04 12:00:00", periods=3, freq="5s")
f["estimated_mpp_W"] = [2.0, 4.0, 8.0]
edges = analysis.accepted_edges(f)
self.assertAlmostEqual(analysis.integrate_edges(f, f.estimated_mpp_W, edges), .0125, places=12)
selected = (f.timestamp >= pd.Timestamp("2026-11-04 12:00:05")).to_numpy()
self.assertEqual(int(analysis.accepted_edges(f, selected).sum()), 1)
def test_tel_counters_are_audited_without_treating_gaps_as_reboots(self):
self.assertEqual(len(self.telemetry), 5026)
self.assertEqual(int(self.telemetry.uptime_s.diff().lt(0).sum()), 0)
self.assertEqual(int(self.telemetry.delta.lt(0).sum()), 0)
self.assertEqual(int(self.telemetry.timestamp.diff().dt.total_seconds().gt(15).sum()), 5)
def test_event_provenance_and_command_free_phase(self):
event_table = analysis.make_events(self.events, self.frame, self.telemetry)
self.assertEqual(int(event_table.event_type.eq("sampling_gap").sum()), 5)
for timestamp, message in (("12:27:20", "[CLOUD] Delay=4"),
("12:32:02", "[CLOUD] ErrMax=1100"),
("13:53:00", "[CLOUD] ErrMax=1000"),
("15:12:42", "[CLOUD] Set Coords X:115000, Y:79000")):
match = self.events.loc[self.events.message.eq(message)]
self.assertEqual(len(match), 1)
self.assertEqual(match.timestamp.iloc[0], pd.Timestamp(f"2026-11-04 {timestamp}"))
inside = self.events.loc[(self.events.timestamp > pd.Timestamp("2026-11-04 12:32:02")) &
(self.events.timestamp < pd.Timestamp("2026-11-04 13:53:00"))]
self.assertFalse(inside.message.str.contains("[CLOUD]", regex=False).any())
def test_ff_sensitivity_and_equal_aperture_silicon_scaling(self):
low_ff = analysis.calibrate(self.frame, fill_factor=.70)
default = analysis.metrics(self.frame)
low = analysis.metrics(low_ff)
self.assertAlmostEqual(low["estimated_mpp_energy_Wh"] / default["estimated_mpp_energy_Wh"], .70 / .82, places=12)
self.assertAlmostEqual(low["max_reference_ratio_pct"], default["max_reference_ratio_pct"], places=10)
self.assertAlmostEqual(low["p99_reference_ratio_pct"], default["p99_reference_ratio_pct"], places=10)
self.assertAlmostEqual(default["silicon_21_energy_Wh"] / default["silicon_19_energy_Wh"], 21 / 19, places=12)
self.assertAlmostEqual(default["silicon_19_energy_Wh"],
default["irradiation_Wh_m2"] * analysis.APERTURE_M2 * .19, places=12)
self.assertAlmostEqual(default["cpv_to_silicon_19_ratio_pct"],
100 * default["estimated_mpp_energy_Wh"] / default["silicon_19_energy_Wh"], places=12)
def test_scenario_area_scaling_and_fill_factor_validation(self):
default = analysis.metrics(self.frame)
with patch.object(analysis, "APERTURE_M2", 2 * analysis.APERTURE_M2):
doubled_area = analysis.metrics(self.frame)
self.assertAlmostEqual(doubled_area["silicon_19_energy_Wh"], 2 * default["silicon_19_energy_Wh"])
self.assertAlmostEqual(doubled_area["cpv_to_silicon_19_ratio_pct"], default["cpv_to_silicon_19_ratio_pct"] / 2)
for invalid in (0, -0.1, 1.01):
with self.assertRaises(ValueError):
analysis.calibrate(self.frame, fill_factor=invalid)
def test_invalid_adc_observations_have_no_reconstructed_power(self):
raw = self.frame.iloc[:3].copy()
codes = [analysis.ADC_MAX, int(.995 * analysis.ADC_MAX), -1]
raw["c_raw"] = codes
calibrated = analysis.calibrate(raw)
self.assertEqual(calibrated.c_raw.tolist(), codes)
self.assertFalse(calibrated.accepted.any())
self.assertTrue(calibrated.estimated_mpp_W.isna().all())
self.assertTrue(calibrated.responsivity_A_m2_W.isna().all())
def test_rejected_middle_observation_never_creates_a_bridge(self):
f = self.frame.iloc[:3].copy().reset_index(drop=True)
f.loc[1, "accepted"] = False
edges = analysis.accepted_edges(f)
self.assertFalse(edges.any())
self.assertEqual(analysis.integrate_edges(f, f.estimated_mpp_W, edges), 0)
def test_empirical_reference_anomaly_is_retained_and_sensitivity_reported(self):
idx = self.frame.responsivity_A_m2_W.idxmax()
row = self.frame.loc[idx]
self.assertEqual(int(row.source_line), 11153)
self.assertEqual(int(row.c_signed), 3_591_330)
self.assertAlmostEqual(row.irradiance_W_m2, 312.95413886954054, places=9)
self.assertAlmostEqual(row.responsivity_A_m2_W, .00107755977191797, places=14)
m = analysis.metrics(self.frame)
self.assertGreater(m["p99_reference_ratio_pct"], m["max_reference_ratio_pct"])
self.assertAlmostEqual(m["max_reference_ratio_pct"], 28.8996079943, places=8)
self.assertAlmostEqual(m["p99_reference_ratio_pct"], 74.0716361928, places=8)
self.assertEqual(len(self.frame), 5049)
def test_suncalc_transcription_provenance_and_timebase(self):
sun = analysis.suncalc_frame()
self.assertEqual(len(sun), 46)
self.assertEqual(sun.timestamp.iloc[0], pd.Timestamp("2026-11-04 07:00"))
self.assertEqual(sun.timestamp.iloc[-1], pd.Timestamp("2026-11-04 18:15"))
self.assertAlmostEqual(sun.azimuth_deg.iloc[0], 106.87)
self.assertAlmostEqual(sun.solar_elevation_deg.iloc[22], 52.59)
self.assertTrue((sun.latitude_deg == 21.87304).all())
self.assertTrue((sun.longitude_deg == -102.28282).all())
def test_solar_interpolation_does_not_extrapolate(self):
f = self.frame.iloc[:3].copy()
f["timestamp"] = pd.to_datetime(["2026-11-04 06:59:00", "2026-11-04 18:16:00", "2026-11-04 12:22:30"])
result = analysis.add_solar_context(f, analysis.suncalc_frame())
self.assertTrue(result.azimuth_deg.iloc[:2].isna().all())
self.assertAlmostEqual(result.azimuth_deg.iloc[2], (173.01 + 178.93) / 2)
self.assertAlmostEqual(result.solar_elevation_deg.iloc[2], (52.35 + 52.59) / 2)
if __name__ == "__main__":
unittest.main(verbosity=2)