"""Scientific regression and independent cross-checks; no website dependencies."""
import unittest
from fractions import Fraction as F
from dataclasses import replace
import math
import numpy as np
from scipy.linalg import expm
from source import AmplificationSource, IdentityChannel, ErrorBounds, fixed_rate_window
from observation import *
from certificates import *
from example import censored_wells, selection_example, plateau_normalized_ratio


class ScientificChecks(unittest.TestCase):
    def test_01_source_atom_generator_and_exact_uniformization(self):
        for source in [AmplificationSource(),AmplificationSource(3,F(4),F(2),F(1)),AmplificationSource(1,F(1))]:
            for t in [F(0),F(3,2),F(5)]:
                b=source.certify(t,80);numeric=source.numerical_errors(float(t))
                for interval,x in zip([b.blank,b.miss],numeric):
                    self.assertLessEqual(float(interval[0])-2e-14,x);self.assertLessEqual(x,float(interval[1])+2e-14)
                    self.assertLess(float(interval[1]-interval[0]),1e-12)
            self.assertAlmostEqual(source.numerical_errors(0)[1],1-source.initial_hit(),14)
        # Repeated rates (2,2,1.5) cause no spectral singularity.
        repeated=AmplificationSource(3,F(4),F(2),F(2,3));self.assertEqual(repeated.rates()[0],repeated.rates()[1])
        self.assertLess(float(repeated.certify(2,80).blank[1]-repeated.certify(2,80).blank[0]),1e-12)
        source=AmplificationSource();rng=np.random.default_rng(571)
        initial,times=source.sample_hit_times(50000,rng)
        self.assertTrue(np.all(times[initial>=5]==0))
        self.assertLess(abs(np.mean(times>7)-source.numerical_errors(7)[1]),.004)
        with self.assertRaises(ValueError):AmplificationSource(5,4)
        with self.assertRaises(ValueError):source.certify(10000)

    def test_02_timing_obstruction_identity_and_uncertainty(self):
        s=AmplificationSource();b=s.certify(5);self.assertTrue(b.separates_all_timing())
        self.assertGreater(b.miss[0],F(69,1000))
        opt=s.numerical_optimum();self.assertAlmostEqual(opt['deadline'],4.4593727511,8);self.assertAlmostEqual(opt['miss'],.0895656871,8)
        b7=s.certify(7);channel=IdentityChannel();joint=channel.guaranteed_errors(b7)
        self.assertLess(joint['blank_upper'],F(3,10000));self.assertLess(joint['miss_upper'],F(422443,10**7))
        fixture=channel.joint_fixture(s,7)
        for p in fixture.values():self.assertAlmostEqual(sum(p),1,14);self.assertGreaterEqual(min(p),0)
        self.assertLessEqual(1-fixture['loaded'][0],float(joint['miss_upper'])+1e-14)
        self.assertAlmostEqual(fixture['loaded'][2],s.numerical_errors(7)[1],14)
        robust=channel.guaranteed_errors(fixed_rate_window(s,F(699,100),F(701,100)))
        self.assertLess(robust['miss_upper'],F(4525192,10**8));self.assertLess(robust['blank_upper'],F(32,100000))
        self.assertEqual(fixed_rate_window(replace(s,capacity=F(10)),F(319,100),F(321,100)).deadline_status(),'usable')
        self.assertEqual(ErrorBounds((F(0),F(1)),(F(0),F(1)),(F(0),F(1))).deadline_status(),'unresolved')
        dual=timing_dual_certificate();self.assertEqual(len(dual['cells']),4)

    def test_03_large_threshold_and_limit_laws(self):
        c=large_threshold_certificate()
        self.assertGreater(c['separating_miss_lower'],F(563,10000));self.assertLess(c['identity_miss_upper'],F(413,10000))
        self.assertLess(c['reserve_blank_upper'],F(1,100));self.assertLess(c['reserve_miss_upper'],F(1,20))
        t=explicit_deadlines(10**6)
        for key,value in [('zero_headroom_separation',29.52814),('identity',32.92934),('double_capacity',15.71263)]:self.assertAlmostEqual(t[key],value,5)
        t=explicit_deadlines(10**8);self.assertAlmostEqual(t['double_capacity'],20.31780,5)
        self.assertAlmostEqual(LimitLaw().optimum()['miss'],.0396249,7)
        self.assertAlmostEqual(LimitLaw(headroom_zero=True).optimum()['miss'],.1091390,7)
        with self.assertRaises(ValueError):explicit_deadlines(999999)

    def test_04_capacity_algebra_block_generator_and_nonidentifiability(self):
        paired=PairedCapacity()
        for R,p in [(5,F(603,1004)),(7,F(335,736)),(10,F(268,669))]:
            self.assertEqual(paired.probability(R),p);self.assertEqual(paired.capacity_set((p,p))['capacity'],[R,R])
        block=BlockCapacity();self.assertEqual(block.probability(7),F(111503293,331058412))
        # Independent Kronecker linear-system check, including a nonunit ratio.
        for ratio in [F(1),F(3,2)]:
            model=replace(block,ratio=ratio);q=np.array([float((F(1,100)+z)*(1-F(z,7))) for z in range(1,5)])
            U=np.diag(-q[:2])+np.diag(q[:1],1);V=np.diag(-q[2:])+np.diag(q[2:3],1)
            K=np.kron(U,np.eye(2))+float(ratio)*np.kron(np.eye(2),V)
            p=np.linalg.solve(-K,np.kron([0,q[1]],np.ones(2)))[0]
            self.assertAlmostEqual(float(model.probability(7)),p,14)
        outer=block.capacity_set((F(32,100),F(35,100)))['capacity'];self.assertLess(outer[0],7);self.assertGreater(outer[1],7)
        self.assertGreaterEqual(block.probability(outer[0]),F(35,100));self.assertLessEqual(block.probability(outer[1]),F(32,100))
        r6=AmplificationSource(3,6,2,1);r4=AmplificationSource(3,4,F(8,3),F(2,3))
        self.assertEqual(sorted(r6.rates()),sorted(r4.rates()))
        for t in [.1,1,3]:self.assertAlmostEqual(r6.numerical_errors(t)[0],r4.numerical_errors(t)[0],14)

    def test_05_censoring_confidence_and_physical_domain(self):
        model=PairedCapacity();rng=np.random.default_rng(20260916)
        for R,horizon,frame,counts in [(7,8,.002,(1752,2229,19)),(7,.12,.05,(478,639,2883)),(100,8,.002,(1367,2600,33))]:
            result,rows=censored_wells(model,4000,rng,R,horizon,frame)
            self.assertEqual(tuple(result[x] for x in ['positive','negative','unknown']),counts)
            self.assertEqual(sum(counts),len(rows));self.assertLessEqual(result['capacity'][0],R)
            if result['capacity'][1] is not None:self.assertGreaterEqual(result['capacity'][1],R)
        self.assertEqual(model.capacity_set(('.1','.2'))['status'],'model incompatibility')
        self.assertEqual(model.capacity_set((0,1))['status'],'unresolved')
        self.assertTrue(model.capacity_set((0,1))['lower_open'])
        p=model.probability(7);uncertain=model.capacity_set((p,p),('.009','.011'))['capacity']
        self.assertLess(uncertain[0],7);self.assertGreater(uncertain[1],7)
        all_unknown=CensoredComparison(0,0,100).confidence();self.assertEqual(all_unknown,(0,1))
        c=CensoredComparison(100,0,0);a=c.confidence();b=c.confidence(mean_error=F(1,10));self.assertLess(b[0],a[0])
        for r in selection_example(model):self.assertGreater(r['selected'],r['unselected'])

    def test_06_signal_and_crossing_containment(self):
        calibration=SignalCalibration();self.assertEqual(calibration.state_interval(1110),(909091,1133333))
        self.assertIsNone(calibration.state_interval(1110,capacity=100))
        self.assertIsNone(calibration.state_interval(-100))
        rng=np.random.default_rng(5706)
        for _ in range(100):
            z=int(rng.integers(0,2000000));B=F(int(rng.integers(950,1051)),10);K=F(int(rng.integers(900,1101)),10**6);error=F(int(rng.integers(-50,51)),10)
            interval=calibration.state_interval(B+K*z+error);self.assertLessEqual(interval[0],z);self.assertGreaterEqual(interval[1],z)
        first=DurationInterval(2,'2.1');second=DurationInterval('3.5','3.6');third=DurationInterval('4.9',5)
        U=DurationInterval.from_crossings(first,second);V=DurationInterval.from_crossings(second,third)
        self.assertEqual((U.lower,U.upper),(F('1.4'),F('1.6')));self.assertEqual(compare_intervals(U,V),'unknown')
        self.assertEqual(compare_intervals(DurationInterval(0,None),DurationInterval(0,None)),'unknown')
        self.assertEqual(crossing_bracket([0,1],[(0,2),(5,7)],5)['interval'],DurationInterval(0,1))
        self.assertEqual(crossing_bracket([0,1],[(5,7),(0,2)],5)['status'],'model incompatibility')
        self.assertEqual(crossing_bracket([0],[(5,7)],5)['status'],'initially above')

    def test_07_loading_floor_and_sample_design(self):
        # Occupied false negatives plus empty-source indistinguishability cannot
        # beat this floor, even with perfect detection of every occupied source.
        empty_floor=math.exp(-4)*.99;self.assertAlmostEqual(empty_floor,.018132482499846838,14)
        n=[math.ceil(math.log(.025)/math.log(1-p)) for p in [.01,.05,.0003]]
        self.assertEqual(n,[368,72,12295])
        model=PairedCapacity();sizes=[]
        for R in [7,20,100]:
            gap=min(model.probability(F(9,10)*R)-model.probability(R),model.probability(R)-model.probability(F(11,10)*R))
            sizes.append(math.ceil(math.log(40)/(2*float(gap)**2)))
        self.assertEqual(sizes,[4883,225627,9911503])
        self.assertEqual([round(plateau_normalized_ratio(R),8) for R in [100,1000,10000,100000]],[.81814128,.82591487,.82669366,.82677156])


if __name__=='__main__':unittest.main()
