"""Public numerical checks for the supplied P06 convolution; not a qualification."""
import copy
import importlib.util
import math
from pathlib import Path
import unittest

HERE = Path(__file__).resolve().parent
spec = importlib.util.spec_from_file_location('conv', HERE/'perception-convolution.py')
c = importlib.util.module_from_spec(spec)
spec.loader.exec_module(c)


def record(split='training'):
    return {'id': 'tiny', 'width': 2, 'height': 2, 'rgb': [255, 0, 127]*4,
            'trimap': [1, 2, 3, 1], 'split': split}


class ConvolutionChecks(unittest.TestCase):
    def test_patch_order_padding_and_shared_filter(self):
        r = record()
        x = c.patch(r, 0)
        self.assertEqual(len(x), 27)
        self.assertEqual(x[:12], [0.]*12)
        self.assertEqual(x[12:15], [1, -1, 127/127.5-1])
        weights = [0.]*59
        weights[12], weights[56] = 2, 3
        for i in range(4):
            h, z, p = c.forward(weights, c.patch(r, i))
            self.assertEqual(h, [math.tanh(2), 0])
            self.assertEqual(z, 3*math.tanh(2))
            self.assertAlmostEqual(p, 1/(1+math.exp(-z)), places=14)

    def test_every_gradient_and_simultaneous_step(self):
        rows = c.rows_for([record()], 'training')
        weights = c.initial()
        _, gradient = c.objective(weights, rows)
        for j in range(59):
            plus, minus = weights[:], weights[:]
            plus[j] += 1e-5
            minus[j] -= 1e-5
            numerical = (c.objective(plus, rows)[0]-c.objective(minus, rows)[0])/2e-5
            self.assertAlmostEqual(gradient[j], numerical, places=8)
        actual, _ = c.train([record()], 1, .3)
        self.assertEqual(actual, [w-.3*g for w, g in zip(weights, gradient)])

    def test_split_and_ignored_target_semantics(self):
        training, development = record(), record('development')
        expected = c.train([training, development], 2)
        development['rgb'] = [0]*12
        development['trimap'] = [1]*4
        self.assertEqual(expected, c.train([training, development], 2))
        self.assertEqual(len(c.rows_for([training], 'training')), 3)
        # Ignored pixels are not targets but legitimately affect neighbors' RGB context.
        changed = copy.deepcopy(training)
        changed['rgb'][6:9] = [0, 255, 0]
        self.assertNotEqual(expected, c.train([changed], 2))

    def test_learning_determinism_and_bounds(self):
        first, trace = c.train([record()], 10)
        self.assertLess(trace['finalTrainingLoss'], trace['initialTrainingLoss'])
        self.assertEqual((first, trace), c.train([record()], 10))
        for n in (0, 101, True, 1.5):
            with self.assertRaises(ValueError): c.train([record()], n)
        for weights in ([0]*58, [float('nan')]*59, [51]*59, [True]*59):
            with self.assertRaises(ValueError): c.validate(weights)

    def test_real_asset_replay_ignores_claims(self):
        path = HERE/'subset.json'
        if not path.exists(): path = HERE.parent/'data/pets/subset.json'
        data = c.b.load_data(path)
        artifact = {'version': c.VERSION, 'sourceHash': c.b.SOURCE_HASH, 'weights': c.initial(),
                    'threshold': .5, 'images': 'forged', 'trainingLossRecomputed': -1}
        result = c.replay(data, artifact)
        self.assertEqual(len(result['images']), 8)
        self.assertGreater(result['trainingLossRecomputed'], 0)
        self.assertEqual(result, c.replay(data, result))
        self.assertEqual(result['freshSourceEvaluation'], 'pending')


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