"""Exact chemistry/source checks and separately evaluated operating bounds."""
import unittest
from fractions import Fraction as Q
import numpy as np
import sympy as sp
from example import (CompletedExporter,OperatingCertificate,PhysicalAccounting,FiniteBath,
    EPSILON,ETA,BOLTZMANN,reference)

class ScienceTests(unittest.TestCase):
    def test_common_chemistry_and_cycle_ranks(self):
        for release in (19,20,21):
            for delta in ('1/50','3/100','1/25'):
                result=CompletedExporter(release,delta).certificates()
                self.assertEqual(result['rate_ratios'],['10','1','1','10','1','8000000000'])
                self.assertEqual((result['internal_rank'],result['full_rank']),(4,5))
                self.assertTrue(result['unit_activity_marked_source_identity'])
        self.assertEqual(BOLTZMANN[7]/BOLTZMANN[6],80000000000)

    def test_symbolic_label_projection_and_deletion(self):
        counts=sp.symbols('u w x c1 c2 z');V=173
        for enabled in (True,False):
            completed=CompletedExporter(enabled=enabled);source=reference.DrivenReactor(completed.p,enabled)
            self.assertEqual(set(completed.by_label),set(source.by_label))
            for c in completed.channels:
                original=source.by_label[c.label]
                self.assertEqual(sp.expand(c.count_rate(counts,V)-original.count_rate(counts,V)),0)
                self.assertEqual(c.jump,original.jump);self.assertEqual(c.marks,original.marks)
        enabled=CompletedExporter();disabled=CompletedExporter(enabled=False)
        self.assertEqual(set(enabled.by_label)-set(disabled.by_label),{6,7})
        self.assertEqual(enabled.by_label[18],disabled.by_label[18]);self.assertEqual(enabled.by_label[19],disabled.by_label[19])
        self.assertEqual(enabled.by_label[9].count_rate((1,1,1,0,0,0),100),0)
        self.assertEqual(enabled.by_label[9].count_rate((1,1,2,0,0,0),100),Q(2,5))

    def test_neighboring_detailed_balance(self):
        for model in (CompletedExporter(),CompletedExporter(fuel='3/2',waste='2/3')):
            for surplus in ((0,0,0,0,0,0),(3,5,2,4,1,7),(8,1,12,2,4,3)):
                for j in range(6):
                    result=model.neighboring_ratio(j,surplus,31)
                    self.assertEqual(Q(result['propensity_ratio']),Q(result['count_potential_factor'])*Q(result['reservoir_factor']))
        release=CompletedExporter().neighboring_ratio(4,(0,0,0,0,0,0),31)
        self.assertEqual(Q(release['propensity_ratio']),Q(31,2))  # No extra factor of 1/2 in association.

    def test_interval_probabilities_and_scope(self):
        model=CompletedExporter()
        for V,claim,exponent in [(10**8,'0.923849',21699),(2*10**8,'0.994201',43414),(3*10**8,'0.999558',65128)]:
            budget=OperatingCertificate(V,100,model);result=budget.evaluate()
            self.assertEqual(result['success_lower_certified_six_decimals'],claim)
            self.assertGreaterEqual(result['disabled']['certified_less_than_10_power_minus'],exponent)
        self.assertFalse(OperatingCertificate(10**8,100,model).passes('1/100'))
        self.assertIsNone(OperatingCertificate(1,1,model).evaluate()['disabled']['certified_less_than_10_power_minus'])
        self.assertNotIn('windows',OperatingCertificate(100,'1/2',model).logs())
        self.assertIsNone(OperatingCertificate(10**8,'201/2',model).evaluate()['disabled'])
        for invalid in (CompletedExporter(fuel='101/100'),CompletedExporter(delta='1/5'),CompletedExporter(enabled=False)):
            with self.assertRaises(ValueError):OperatingCertificate(10**8,100,invalid)

    def test_initiation_and_sufficient_sizing(self):
        model=CompletedExporter();food=(31,31,0,0,0,0)
        active=[c.label for c in model.channels if c.count_rate(food,31)>0]
        self.assertEqual(active,[0,10,11,12,13,19])
        self.assertEqual(sum(model.by_label[i].count_rate(food,31) for i in (0,19)),31*(EPSILON+model.delta*ETA))
        self.assertEqual(OperatingCertificate.necessary_scale('1/100'),4593686)
        sizes=OperatingCertificate.sufficient_scales(100,'1/100',model)
        self.assertEqual(sizes['simple_sufficient_V'],264915869);self.assertEqual(sizes['full_budget_interval_checked_V'],178838783)
        self.assertTrue(OperatingCertificate(178838783,100,model).passes('1/100'));self.assertFalse(OperatingCertificate(178838782,100,model).passes('1/100'))

    def test_accounting_and_conditional_bath(self):
        p=PhysicalAccounting(10**8,100).evaluate(CompletedExporter())
        self.assertAlmostEqual(float(p['volume_pL']),.1660539067173847)
        self.assertEqual(p['each_food_allowance'],'120000000000');self.assertEqual(p['gross_driven_allowance'],'7500000000')
        self.assertEqual(p['template_equivalents_per_window'],'5000');self.assertEqual(p['service_per_equivalent_upper'],'15000')
        self.assertEqual(p['conditional_bath_each_stock'],750000000000)
        self.assertAlmostEqual(float(p['chemical_work_bound_nJ']),.775076634700918)
        bath=FiniteBath(1000)
        for label in [18]*7+[19]*3:bath.exchange(label)
        self.assertEqual(bath.gross,10);self.assertEqual(bath.activities,(Q(249,250),Q(251,250)))
        changed=CompletedExporter(fuel=bath.activities[0],waste=bath.activities[1])
        self.assertFalse(changed.certificate_scope())

    def test_full_dynamics_and_natural_ledger(self):
        model=CompletedExporter();sol=model.deterministic(1);check=model.deterministic(1,'BDF');times=np.linspace(0,501,51)
        states=sol.sol(times).T;self.assertLess(np.max(abs(states-check.sol(times).T)),2e-7)
        for row in states:
            counts=row[:6];E,IU,IW,QF,QP,OA,OB,MW=row[6:]
            self.assertAlmostEqual(np.dot(reference.A_WEIGHTS,counts)+OA,1+IU,places=8)
            self.assertAlmostEqual(np.dot(reference.B_WEIGHTS,counts)+OB,1+IW,places=8)
        policy=reference.ObservationPolicy(1000000,1);monitor=reference.OperatingMonitor(model,policy)
        monitor.event(.01,0);monitor.event(.02,18);monitor.event(.03,19);monitor.event(.04,14)
        self.assertEqual(monitor.ledger[:5],[4,0,0,1,1]);monitor.audit_ledger()
        limited=model.stochastic(policy,limit=10,seed=41)
        if limited['status']=='unfinished':self.assertIsNone(limited['event_verdict'])

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