import unittest
from fractions import Fraction as Q
import numpy as np
from chemistry import *
from finite_bath import *
from certificates import *


class ScientificChecks(unittest.TestCase):
    def test_finite_bath_generator_and_boundary_rates(self):
        r=FiniteBathReactor();N=(20,30,10,3,4,5)
        for f in [0,1,40,100]:
            b=Bath(100,f,100-f)
            self.assertEqual(r.exact_drift(N,100,b,A),100-dot(A,N));self.assertEqual(r.exact_drift(N,100,b,B),100-dot(B,N))
            rates=r.propensities(N,100,b)
            if f==0:self.assertEqual(rates[18],0)
            if f==100:self.assertEqual(rates[19],0)
            self.assertAlmostEqual(float(rates[9]),float(r.r*N[2]*(N[2]-1)/100))
        zero=FiniteBathReactor(drive='0');self.assertTrue(zero.admitted(Bath(1,1000,1000)))
        self.assertEqual(zero.propensities(N,100,Bath(100,50,50))[18:],[0,0])

    def test_joint_pulse_and_actual_count_history_ledger(self):
        pulse=MolecularPulse();N=(0,0,2,0,0,0);out=list(pulse.enumerate(N,4,Intervention()))
        self.assertEqual(sum(p for _,p in out),1)
        self.assertTrue(any(o.retained[2]==0 for o,p in out if p))
        r=FiniteBathReactor();res=CountMission(r).run((0,0,20,0,0,0),20,Bath(200,200,0),3,HistoryController(),91,30000)
        self.assertEqual(res['status'],'complete')
        for a,b in zip(res['history'],res['history'][1:]):self.assertEqual(a['bath'],b['incoming_bath']);self.assertEqual(a['endpoint'],b['start'])
        for h in res['history']:self.assertEqual(h['inventory_residual'],0)
        self.assertEqual(res['history'][-1]['bath']['fuel'],200-res['net_service'])
        self.assertLessEqual(200-res['bath_prefix_fuel_min'],res['total_forward']+res['total_reverse'])

    def test_metered_food_and_unfinished_budget_are_not_success(self):
        run=CountMission(FiniteBathReactor());args=((0,0,20,0,0,0),20,Bath(200,200,0),2,HistoryController(),4)
        self.assertEqual(run.run(*args,meter=FoodMeter(1,1))['status'],'food_shutdown_at_pulse')
        cut=run.run(*args,event_budget=0);self.assertEqual(cut['status'],'event_budget_exhausted');self.assertIsNone(cut['success'])
        a=run.run(*args);b=run.run(*args,meter=FoodMeter(10000,10000))
        self.assertEqual(a['history'],b['history'])

    def test_certificate_scope_and_sharpened_rate(self):
        r=FiniteBathReactor();self.assertEqual(Q(1,10**6)*(Q(1,700)-Q(1,1000))-Q(12,5)*91/Q(10**12),KAPPA)
        c=MissionCertificate(96000000000,r,Bath(1,1,0),sharp=True)
        self.assertGreater(Q(c.evaluate(100)['joint_product_lower_rational']),Q(999979,1000000))
        self.assertEqual(c.inventory(100)['net_synthesis_lower'],78257142929)
        self.assertEqual(c.inventory(100,96000000000)['net_synthesis_lower'],78857142929)
        with self.assertRaises(ValueError):MissionCertificate(10**11,r,Bath(1,1,1),sharp=True)
        with self.assertRaises(ValueError):MissionCertificate(10**11,FiniteBathReactor(drive='1/25'),Bath(1,1,0),sharp=True)
        general=MissionCertificate(10**11,r,Bath(1,1,1));self.assertEqual(general.inventory(100)['gross_service_allowance'],2*10**12)

    def test_inverse_design_rounding_and_declared_output(self):
        d=design(100,'21/1000000','1/10')
        self.assertGreaterEqual(Q(d['taylor_lower']),Q(d['taylor_target']));self.assertGreaterEqual(Q(d['R'],10),d['gross_service_allowance'])
        fixed=design(100,'21/1000000','1/10',357142857200,18518518600)
        self.assertGreaterEqual(fixed['V'],2*10**11)
        self.assertGreaterEqual(100*ceildiv(fixed['V'],56),357142857200)
        self.assertGreaterEqual(loaded_force_capacity(100,'1/10'),2001)

    def test_neighbour_ratio_including_pure_boundary_and_exact_energy(self):
        r=FiniteBathReactor();res=neighbour_balance((2,3,4,0,0,0),100,Bath(10,10,0),r)
        self.assertEqual(res['post_forward_waste'],1)
        for J in range(11):
            record=bath_energy(10,J);got=sp.sympify(record['delta_G_over_kBT'])
            want=-J*sp.log(80000000000)-sp.log(sp.binomial(10,J))
            self.assertEqual(sp.simplify(got.doit()-want),0)
        with self.assertRaises(ValueError):neighbour_balance((2,3,4,0,0,0),100,Bath(10,10,0),FiniteBathReactor(drive='0'))

    def test_density_ledgers_and_zero_drive_production(self):
        c=Q(1,28);initial=[Q(159,160)-2*c,Q(159,160)-2*c,0,0,c,0,1,0]
        res=DensityMission(FiniteBathReactor(drive='0'),1,1).run(initial,2)
        for h in res['history']:
            self.assertEqual(h['counters']['gross_drive'],0);self.assertGreater(h['counters']['collected_I'],1/56)
            self.assertLess(abs(h['inventory_residual']),1e-10);self.assertLess(abs(h['bath_residual']),1e-10)


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