import unittest
from fractions import Fraction as F
import numpy as np
from reactor import *
from certificates import *


class ScientificTests(unittest.TestCase):
    def test_literal_source_accounts(self):
        model=CofactorReactor(LinearRate(1),LinearRate(1))
        t,y,r=model.run(offset=.01)
        self.assertLess(r['finite_account_residual'],1e-10)
        self.assertLess(r['complex_ledger_residual'],1e-10)
        self.assertLess(r['product_ledger_residual'],1e-10)
        self.assertAlmostEqual(r['integrated_plus_tail_loss_enclosure_numeric'][0],r['source_identity_loss_enclosure_numeric'][0],places=10)
        self.assertTrue(np.all(y[:,0]+y[:,1]<=1+1e-10))
        self.assertTrue(np.all(y[:,1]<=1+1e-10))
        self.assertLess(r['peak_relative_suppression'],float(DesignBox().constants()['suppression']))

    def test_schedules_and_storage(self):
        for c in [.2,1.,20.]:
            model=CofactorReactor(LinearRate(1),LinearRate(1),Reporter(release=c))
            for schedule in [AssociationSchedule(),AssociationSchedule(windows=((4.,8.),)),AssociationSchedule(fade_rate=1.)]:
                t,y,r=model.run(schedule,30.)
                self.assertAlmostEqual(r['independent_integral_ratio_numeric'],(1+1/c)/2,places=9)
        self.assertEqual(1+F(1)/F(1,5),6)
        with self.assertRaises(ValueError):AssociationSchedule(windows=((0,6),(4,8)))

    def test_exact_joint_design_and_distinct_contracts(self):
        b=DesignBox();rounded=b.contract(rounded=True);general=b.contract()
        self.assertEqual(b.constants()['suppression'],F(400869,8492000))
        self.assertEqual(rounded.margin,F(1214759671,218587500000))
        self.assertEqual(rounded.AL,F(47745467,83750000))
        self.assertTrue(rounded.certified);self.assertNotEqual(rounded.threshold,general.threshold)
        self.assertFalse(b.contract(F(6),True).certified);self.assertTrue(b.contract(F(7),True).certified)
        with self.assertRaises(ValueError):DesignBox(release=(F(1),F(2))).contract()

    def test_inversion_and_promise(self):
        c=DesignBox().contract(rounded=True)
        for p in [F(95,100),F(1),F(3,2),F(2),F(205,100)]:
            for y in [c.AL*p/(p+c.box.native[1])-c.box.error,c.AU*p/(p+c.box.native[0])+c.box.error]:
                r=c.inverse(y);self.assertLessEqual(r['p'][0],p);self.assertGreaterEqual(r['p'][1],p)
        self.assertEqual(c.classify(F('0.30549'),True),'low')
        self.assertEqual(c.classify(F('0.41151'),True),'high')
        self.assertEqual(c.classify(c.threshold,True),'incompatible with two-class promise')
        self.assertEqual(c.classify(F('0.30549')),'unresolved')
        self.assertIsNone(c.inverse(-1)['p']);self.assertIsNone(c.inverse(1)['p'])

    def test_fading_recovery_and_tail(self):
        rec=RecoveryCertificate();self.assertLess(rec.ideal(5),F(15,10**7))
        for gamma,n0,n1,t in [('0.5','0.5','0.5','22.7'),('1','1','1','11.2'),('2','1.85','2','6.8'),('5','1.93','5','5.2'),('10','1.93','10','4.9'),('21','1.93','12','4.9')]:
            g,n0,n1,t=map(F,(gamma,n0,n1,t))
            self.assertLess(rec.fading(g,t,n0,n1)['loss_upper'],F(3,10**6))
            self.assertGreater(rec.fading(g,t-F(1,10),n0,n1)['envelope_lower'],F(3,10**6))
        model=CofactorReactor(LinearRate(1),LinearRate(1));s=AssociationSchedule(fade_rate=1.)
        short=model.run(s,5.)[2];long=model.run(s,40.)[2]
        self.assertGreater(short['remaining_association_upper'],0)
        self.assertLessEqual(short['complete_product_enclosure_numeric'][0],long['complete_product_enclosure_numeric'][0]+1e-10)
        self.assertGreaterEqual(short['complete_product_enclosure_numeric'][1],long['complete_product_enclosure_numeric'][1]-1e-10)

    def test_nonlinear_bounds(self):
        self.assertEqual(slope_sandwich(1,1,1,1,20),(F(21,40),F(21,40)))
        lo,hi=slope_sandwich(F(100,121),1,F(100,121),1,20)
        t,y,r=CofactorReactor(MichaelisMenten(10,10),MichaelisMenten(10,10)).run(recovery=40.)
        ratio=y[-1,3]/y[-1,2];self.assertLess(float(lo),ratio);self.assertLess(ratio,float(hi))
        self.assertAlmostEqual(ratio,.5236,places=4)
        with self.assertRaises(ValueError):slope_sandwich(1,1,1,2,1)

    def test_background_alias(self):
        traces=[];flux=[]
        for k,l in [(.5,1.5),(1.5,.5)]:
            m=CofactorReactor(LinearRate(1),LinearRate(k),background=l)
            t,y,r=m.run(AssociationSchedule(fade_rate=1.),20.);traces.append(y[:,:3]);flux.append(k*m.stationary())
        self.assertLess(np.max(abs(traces[0]-traces[1])),1e-10)
        self.assertAlmostEqual(flux[0],1/6);self.assertAlmostEqual(flux[1],1/2)


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