# Copyright (c) ONNX Project Contributors

# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations

import numpy as np

import onnx
from onnx.backend.test.case.base import Base
from onnx.backend.test.case.model import expect
from onnx.defs import AI_ONNX_PREVIEW_TRAINING_DOMAIN, ONNX_DOMAIN


class Gradient(Base):
    @staticmethod
    def export_gradient_scalar_add() -> None:
        add_node = onnx.helper.make_node("Add", ["a", "b"], ["c"], name="my_add")
        gradient_node = onnx.helper.make_node(
            "Gradient",
            ["a", "b"],
            ["dc_da", "dc_db"],
            name="my_gradient",
            domain=AI_ONNX_PREVIEW_TRAINING_DOMAIN,
            xs=["a", "b"],
            y="c",
        )

        a = np.array(1.0).astype(np.float32)
        b = np.array(2.0).astype(np.float32)
        c = a + b
        # dc / da = d(a+b) / da = 1
        dc_da = np.array(1).astype(np.float32)
        # db / db = d(a+b) / db = 1
        dc_db = np.array(1).astype(np.float32)

        graph = onnx.helper.make_graph(
            nodes=[add_node, gradient_node],
            name="GradientOfAdd",
            inputs=[
                onnx.helper.make_tensor_value_info("a", onnx.TensorProto.FLOAT, []),
                onnx.helper.make_tensor_value_info("b", onnx.TensorProto.FLOAT, []),
            ],
            outputs=[
                onnx.helper.make_tensor_value_info("c", onnx.TensorProto.FLOAT, []),
                onnx.helper.make_tensor_value_info("dc_da", onnx.TensorProto.FLOAT, []),
                onnx.helper.make_tensor_value_info("dc_db", onnx.TensorProto.FLOAT, []),
            ],
        )
        opsets = [
            onnx.helper.make_operatorsetid(ONNX_DOMAIN, 12),
            onnx.helper.make_operatorsetid(AI_ONNX_PREVIEW_TRAINING_DOMAIN, 1),
        ]
        model = onnx.helper.make_model_gen_version(
            graph, producer_name="backend-test", opset_imports=opsets
        )
        expect(
            model, inputs=[a, b], outputs=[c, dc_da, dc_db], name="test_gradient_of_add"
        )

    @staticmethod
    def export_gradient_scalar_add_and_mul() -> None:
        add_node = onnx.helper.make_node("Add", ["a", "b"], ["c"], name="my_add")
        mul_node = onnx.helper.make_node("Mul", ["c", "a"], ["d"], name="my_mul")
        gradient_node = onnx.helper.make_node(
            "Gradient",
            ["a", "b"],
            ["dd_da", "dd_db"],
            name="my_gradient",
            domain=AI_ONNX_PREVIEW_TRAINING_DOMAIN,
            xs=["a", "b"],
            y="d",
        )

        a = np.array(1.0).astype(np.float32)
        b = np.array(2.0).astype(np.float32)
        c = a + b
        # d = a * c = a * (a + b)
        d = a * c
        # dd / da = d(a*a+a*b) / da = 2 * a + b
        dd_da = (2 * a + b).astype(np.float32)
        # dd / db = d(a*a+a*b) / db = a
        dd_db = a

        graph = onnx.helper.make_graph(
            nodes=[add_node, mul_node, gradient_node],
            name="GradientOfTwoOperators",
            inputs=[
                onnx.helper.make_tensor_value_info("a", onnx.TensorProto.FLOAT, []),
                onnx.helper.make_tensor_value_info("b", onnx.TensorProto.FLOAT, []),
            ],
            outputs=[
                onnx.helper.make_tensor_value_info("d", onnx.TensorProto.FLOAT, []),
                onnx.helper.make_tensor_value_info("dd_da", onnx.TensorProto.FLOAT, []),
                onnx.helper.make_tensor_value_info("dd_db", onnx.TensorProto.FLOAT, []),
            ],
        )

        opsets = [
            onnx.helper.make_operatorsetid(ONNX_DOMAIN, 12),
            onnx.helper.make_operatorsetid(AI_ONNX_PREVIEW_TRAINING_DOMAIN, 1),
        ]
        model = onnx.helper.make_model_gen_version(
            graph, producer_name="backend-test", opset_imports=opsets
        )
        expect(
            model,
            inputs=[a, b],
            outputs=[d, dd_da, dd_db],
            name="test_gradient_of_add_and_mul",
        )
