"""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)