classdef linear_bilevel
    %{
    Upper-level
    ===========
        minimise    c_upper ' x + d_upper ' y
        subject to  A_upper * x + B_upper * y   >= rhs_upper
                    x                           >= x_lower_bound
                    x                           <= x_upper_bound
                    y solves lower-level

    Lower-level
    ===========
        minimise    c_lower ' x + d_lower ' y
        subject to  A_lower * x + B_lower * y   >= rhs_lower
                    y                           >= y_lower_bound
                    y                           <= y_upper_bound
    %}
    properties(Constant)
        name = 'linear_bilevel';
        category = 'archetype';
        subcategory = '';
        datasets = {
            'anandalingham_white_1990.json'; 'bard_1984a.json'; 'bard_1984b.json'; 'bard_1991_ex2.json';
            'bard_falk_1982_ex2.json'; 'ben_ayed_blair_1990a.json'; 'ben_ayed_blair_1990b.json';
            'bialas_karwan_1984a.json'; 'bialas_karwan_1984b.json'; 'candler_townsley_1982.json';
            'clark_westerberg_1988.json'; 'clark_westerberg_1990b.json'; 'glackin_et_al_2009.json';
            'haurie_savard_white_1990.json'; 'hu_huang_zhang_2009.json'; 'hypercube_10.json'; 'hypercube_100.json';
            'hypercube_3.json'; 'hypercube_50.json'; 'lan_wen_shih_lee_2007.json'; 'liu_hart_1994.json';
            'mersha_dempe_2006_ex1.json'; 'mersha_dempe_2006_ex2.json';'tuy_et_al_1993.json'; 'tuy_et_al_1994.json';
            'tuy_et_al_2007_ex3.json'; 'visweswaran_et_al_1996.json'; 'wang_jiao_li_2005.json'
        };
        paths = fullfile('bolib3', 'data', 'linear_bilevel', linear_bilevel.datasets);
        x0 = [16.0];
        y0 = [11.0];
        required_data_fields = {
            'n_x', 'n_y', ...
            'A_upper', 'B_upper', 'c_upper', 'd_upper', 'rhs_upper', 'x_upper_bound', 'x_lower_bound', ...
            'A_lower', 'B_lower', 'c_lower', 'd_lower', 'rhs_lower', 'y_upper_bound', 'y_lower_bound'
        };
    end


    methods(Static)

        % Upper-level objective function
        % minimise F(x,y) := c'x + d'y
        function obj = F(x, y, data)
            obj = dot(data.c_upper, x) + dot(data.d_upper, y);
        end
        
        % Upper-level inequality constraints
        % Ax + By  >= rhs
        % x        >= x_lower_bound
        % x        <= x_upper_bound
        function ineq = G(x, y, data)
            ineq = [];
            if ~isempty(data.rhs_upper)
                ineq = [ineq; data.A_upper * x + data.B_upper * y - data.rhs_upper];
            end
            if ~isempty(data.x_lower_bound)
                ineq = [ineq; x - data.x_lower_bound];
            end
            if ~isempty(data.x_upper_bound)
                ineq = [ineq; data.x_upper_bound - x];
            end
        end
        
        % Upper-level equality constraints
        function val = H(~, ~, ~)
            val = [];
        end
        
        % Lower-level objective function
        % minimise f(x,y) := c'x + d'y
        function obj = f(x, y, data)
            obj = dot(data.c_lower, x) + dot(data.d_lower, y);
        end
        
        % Lower-level inequality constraints
        function ineq = g(x, y, data)
            ineq = [];
            if ~isempty(data.rhs_lower)
                ineq = [ineq; data.A_lower * x + data.B_lower * y - data.rhs_lower];
            end
            if ~isempty(data.y_lower_bound)
                ineq = [ineq; y - data.y_lower_bound];
            end
            if ~isempty(data.y_upper_bound)
                ineq = [ineq; data.y_upper_bound - y];
            end
        end
        
        % Lower-level equality constraints
        function val = h(~, ~, ~)
            val = [];
        end

        % If the bilevel program is parameterized by data, this function should
        %  provide code to read data file and return an appropriate structure.
        function data = read_data(filepath)
            % Read JSON file
            file = fopen(filepath, 'r');
            raw = fread(file, inf, 'uint8=>char')';
            fclose(file);
            data = jsondecode(raw);

            % Add a field for the filepath
            data.filepath = filepath;

            % Bounds are optional.  If they are not provided we replace them with an empty array
            bounds = {'x_upper_bound', 'x_lower_bound', 'y_upper_bound', 'y_lower_bound'};
            for i = 1:length(bounds)
                if ~isfield(data, bounds{i})
                    data.(bounds{i}) = [];
                end
            end

            % Validate required fields
            for i = 1:length(linear_bilevel.required_data_fields)
                key = linear_bilevel.required_data_fields{i};
                assert(isfield(data, key), ...
                    sprintf("Missing required field '%s' in '%s'.", key, filepath));
            end
        end


        % Key are the function/variable names
        % Values are their dimention
        function n = dimention(key, data)
            n = dictionary( ...
                'x', data.n_x, ...
                'y', data.n_y, ...
                'F', 1, ...
                'G', length(data.rhs_upper) + length(data.x_upper_bound) + length(data.x_lower_bound), ...
                'H', 0, ...
                'f', 1, ...
                'g', length(data.rhs_lower) + length(data.y_upper_bound) + length(data.y_lower_bound), ...
                'h', 0 ...
            );
            if isKey(n,key)
                n = n(key);
            end
        end
    end
end