# ____________________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2026 National Technology and Engineering Solutions of Sandia, LLC
# Under the terms of Contract DE-NA0003525 with National Technology and Engineering
# Solutions of Sandia, LLC, the U.S. Government retains certain rights in this
# software.  This software is distributed under the 3-clause BSD License.
# ____________________________________________________________________________________

#
# Problem Writer for (Free) MPS Format Files
#

import logging

from io import StringIO

from pyomo.common.gc_manager import PauseGC
from pyomo.opt import ProblemFormat
from pyomo.opt.base import AbstractProblemWriter, WriterFactory
from pyomo.core.base import (
    SymbolMap,
    TextLabeler,
    NumericLabeler,
    Constraint,
    SortComponents,
    Var,
    value,
    SOSConstraint,
    Objective,
    ComponentMap,
    is_fixed,
)
from pyomo.repn import generate_standard_repn

logger = logging.getLogger('pyomo.core')


def _no_negative_zero(val):
    """Make sure -0 is never output. Makes diff tests easier."""
    if val == 0:
        return 0
    return val


def _get_bound(exp):
    if exp is None:
        return None
    if is_fixed(exp):
        return value(exp)
    raise ValueError("non-fixed bound or weight: " + str(exp))


@WriterFactory.register('mps', 'Generate the corresponding MPS file')
class ProblemWriter_mps(AbstractProblemWriter):
    def __init__(self, int_marker=False):
        AbstractProblemWriter.__init__(self, ProblemFormat.mps)

        # the MPS writer is responsible for tracking which variables are
        # referenced in constraints, so that one doesn't end up with a
        # zillion "unreferenced variables" warning messages. stored at
        # the object level to avoid additional method arguments.
        # dictionary of id(VarData)->VarData.
        self._referenced_variable_ids = {}

        # Keven Hunter made a nice point about using %.16g in his attachment
        # to ticket #4319. I am adjusting this to %.17g as this mocks the
        # behavior of using %r (i.e., float('%r'%<number>) == <number>) with
        # the added benefit of outputting (+/-). The only case where this
        # fails to mock the behavior of %r is for large (long) integers (L),
        # which is a rare case to run into and is probably indicative of
        # other issues with the model.
        # *** NOTE ***: If you use 'r' or 's' here, it will break code that
        #               relies on using '%+' before the formatting character
        #               and you will need to go add extra logic to output
        #               the number's sign.
        self._precision_string = '.17g'

        self._int_marker = int_marker

    def __call__(self, model, output_filename, solver_capability, io_options):
        # Make sure not to modify the user's dictionary,
        # they may be reusing it outside of this call
        io_options = dict(io_options)

        # Skip writing constraints whose body section is
        # fixed (i.e., no variables)
        skip_trivial_constraints = io_options.pop("skip_trivial_constraints", False)

        # Use full Pyomo component names in the MPS file rather
        # than shortened symbols (slower, but useful for debugging).
        symbolic_solver_labels = io_options.pop("symbolic_solver_labels", False)

        output_fixed_variable_bounds = io_options.pop(
            "output_fixed_variable_bounds", False
        )

        # If False, unused variables will not be included in
        # the MPS file. Otherwise, include all variables in
        # the bounds sections.
        include_all_variable_bounds = io_options.pop(
            "include_all_variable_bounds", False
        )

        labeler = io_options.pop("labeler", None)

        # How much effort do we want to put into ensuring the
        # MPS file is written deterministically for a Pyomo model:
        #    0 : None
        #    1 : sort keys of indexed components (default)
        #    2 : sort keys AND sort names (over declaration order)
        file_determinism = io_options.pop("file_determinism", 1)

        # user defined orderings for variable and constraint
        # output
        row_order = io_options.pop("row_order", None)
        column_order = io_options.pop("column_order", None)

        # make sure the ONE_VAR_CONSTANT variable appears in
        # the objective even if the constant part of the
        # objective is zero
        force_objective_constant = io_options.pop("force_objective_constant", False)

        # Whether or not to include the OBJSENSE section in
        # the MPS file. Some solvers, like GLPK and CBC,
        # either throw an error or flat out ignore this
        # section (I assume the default is to minimize)
        skip_objective_sense = io_options.pop("skip_objective_sense", False)

        if len(io_options):
            raise ValueError(
                "ProblemWriter_mps passed unrecognized io_options:\n\t"
                + "\n\t".join("%s = %s" % (k, v) for k, v in io_options.items())
            )

        if symbolic_solver_labels and (labeler is not None):
            raise ValueError(
                "ProblemWriter_mps: Using both the "
                "'symbolic_solver_labels' and 'labeler' "
                "I/O options is forbidden"
            )

        if symbolic_solver_labels:
            labeler = TextLabeler()
        elif labeler is None:
            labeler = NumericLabeler('x')

        # clear the collection of referenced variables.
        self._referenced_variable_ids.clear()

        if output_filename is None:
            output_filename = model.name + ".mps"

        # when sorting, there are a non-trivial number of
        # temporary objects created. these all yield
        # non-circular references, so disable GC - the
        # overhead is non-trivial, and because references
        # are non-circular, everything will be collected
        # immediately anyway.
        with PauseGC() as pgc:
            with open(output_filename, "w") as output_file:
                symbol_map = self._print_model_MPS(
                    model,
                    output_file,
                    solver_capability,
                    labeler,
                    output_fixed_variable_bounds=output_fixed_variable_bounds,
                    file_determinism=file_determinism,
                    row_order=row_order,
                    column_order=column_order,
                    skip_trivial_constraints=skip_trivial_constraints,
                    force_objective_constant=force_objective_constant,
                    include_all_variable_bounds=include_all_variable_bounds,
                    skip_objective_sense=skip_objective_sense,
                )

        self._referenced_variable_ids.clear()

        return output_filename, symbol_map

    def _extract_variable_coefficients(
        self, row_label, repn, column_data, quadratic_data, variable_to_column
    ):
        #
        # Linear
        #
        if len(repn.linear_coefs) > 0:
            for vardata, coef in zip(repn.linear_vars, repn.linear_coefs):
                self._referenced_variable_ids[id(vardata)] = vardata
                column_data[variable_to_column[vardata]].append((row_label, coef))

        #
        # Quadratic
        #
        if len(repn.quadratic_coefs) > 0:
            quad_terms = []
            for vardata, coef in zip(repn.quadratic_vars, repn.quadratic_coefs):
                self._referenced_variable_ids[id(vardata[0])] = vardata[0]
                self._referenced_variable_ids[id(vardata[1])] = vardata[1]
                quad_terms.append((vardata, coef))
            quadratic_data.append((row_label, quad_terms))

        #
        # Return the constant
        #
        return repn.constant

    def _printSOS(
        self, symbol_map, labeler, variable_symbol_map, soscondata, output_file
    ):
        if hasattr(soscondata, 'get_items'):
            sos_items = list(soscondata.get_items())
        else:
            sos_items = list(soscondata.items())

        if len(sos_items) == 0:
            return

        output_file.write("SOS\n")

        level = soscondata.level

        # I think there are many flavors to the SOS
        # section in the Free MPS format. I'm going with
        # what Cplex and Gurobi seem to recognize
        output_file.write(
            " S%d %s\n" % (level, symbol_map.getSymbol(soscondata, labeler))
        )

        sos_template_string = "    %s %" + self._precision_string + "\n"
        for vardata, weight in sos_items:
            weight = _get_bound(weight)
            if weight < 0:
                raise ValueError(
                    "Cannot use negative weight %f "
                    "for variable %s is special ordered "
                    "set %s " % (weight, vardata.name, soscondata.name)
                )
            if vardata.fixed:
                raise RuntimeError(
                    "SOSConstraint '%s' includes a fixed variable '%s'. This is "
                    "currently not supported. Deactivate this constraint in order to "
                    "proceed." % (soscondata.name, vardata.name)
                )
            self._referenced_variable_ids[id(vardata)] = vardata
            output_file.write(
                sos_template_string % (variable_symbol_map.getSymbol(vardata), weight)
            )

    def _print_model_MPS(
        self,
        model,
        output_file,
        solver_capability,
        labeler,
        output_fixed_variable_bounds=False,
        file_determinism=1,
        row_order=None,
        column_order=None,
        skip_trivial_constraints=False,
        force_objective_constant=False,
        include_all_variable_bounds=False,
        skip_objective_sense=False,
    ):
        symbol_map = SymbolMap()
        variable_symbol_map = SymbolMap()
        # NOTE: we use createSymbol instead of getSymbol because we
        #       know whether or not the symbol exists, and don't want
        #       to the overhead of error/duplicate checking.
        # cache frequently called functions
        extract_variable_coefficients = self._extract_variable_coefficients
        create_symbol_func = SymbolMap.createSymbol
        create_symbols_func = SymbolMap.createSymbols
        alias_symbol_func = SymbolMap.alias
        variable_label_pairs = []

        sortOrder = SortComponents.unsorted
        if file_determinism >= 1:
            sortOrder = sortOrder | SortComponents.indices
            if file_determinism >= 2:
                sortOrder = sortOrder | SortComponents.alphabetical

        #
        # Create variable symbols (and cache the block list)
        #
        all_blocks = []
        variable_list = []
        for block in model.block_data_objects(active=True, sort=sortOrder):
            all_blocks.append(block)

            for vardata in block.component_data_objects(
                Var, active=True, sort=sortOrder, descend_into=False
            ):
                variable_list.append(vardata)
                variable_label_pairs.append(
                    (vardata, create_symbol_func(symbol_map, vardata, labeler))
                )

        variable_symbol_map.addSymbols(variable_label_pairs)

        # and extract the information we'll need for rapid labeling.
        object_symbol_dictionary = symbol_map.byObject
        variable_symbol_dictionary = variable_symbol_map.byObject

        # sort the variable ordering by the user
        # column_order ComponentMap
        if column_order is not None:
            variable_list.sort(key=lambda _x: column_order[_x])

        # prepare to hold the sparse columns
        variable_to_column = ComponentMap(
            (vardata, i) for i, vardata in enumerate(variable_list)
        )
        # add one position for ONE_VAR_CONSTANT
        column_data = [[] for i in range(len(variable_list) + 1)]
        quadobj_data = []
        quadmatrix_data = []
        # constraint rhs
        rhs_data = []

        # print the model name and the source, so we know
        # roughly where
        output_file.write("* Source:     Pyomo MPS Writer\n")
        output_file.write("* Format:     Free MPS\n")
        output_file.write("*\n")
        output_file.write("NAME %s\n" % (model.name,))

        #
        # ROWS section
        #

        objective_label = None
        numObj = 0
        onames = []
        for block in all_blocks:
            gen_obj_repn = getattr(block, "_gen_obj_repn", True)

            # Get/Create the ComponentMap for the repn
            if not hasattr(block, '_repn'):
                block._repn = ComponentMap()
            block_repn = block._repn
            for objective_data in block.component_data_objects(
                Objective, active=True, sort=sortOrder, descend_into=False
            ):
                numObj += 1
                onames.append(objective_data.name)
                if numObj > 1:
                    raise ValueError(
                        "More than one active objective defined for input "
                        "model '%s'; Cannot write legal MPS file\n"
                        "Objectives: %s" % (model.name, ' '.join(onames))
                    )

                objective_label = create_symbol_func(
                    symbol_map, objective_data, labeler
                )

                symbol_map.alias(objective_data, '__default_objective__')
                if not skip_objective_sense:
                    output_file.write("OBJSENSE\n")
                    if objective_data.is_minimizing():
                        output_file.write(" MIN\n")
                    else:
                        output_file.write(" MAX\n")
                # This section is not recognized by the COIN-OR
                # MPS reader
                # output_file.write("OBJNAME\n")
                # output_file.write(" %s\n" % (objective_label))
                output_file.write("ROWS\n")
                output_file.write(" N  %s\n" % (objective_label))

                if gen_obj_repn:
                    repn = generate_standard_repn(objective_data.expr)
                    block_repn[objective_data] = repn
                else:
                    repn = block_repn[objective_data]

                degree = repn.polynomial_degree()
                if degree == 0:
                    logger.warning(
                        "Constant objective detected, replacing "
                        "with a placeholder to prevent solver failure."
                    )
                    force_objective_constant = True
                elif degree is None:
                    raise RuntimeError(
                        "Cannot write legal MPS file. Objective '%s' "
                        "has nonlinear terms that are not quadratic."
                        % objective_data.name
                    )

                constant = extract_variable_coefficients(
                    objective_label, repn, column_data, quadobj_data, variable_to_column
                )
                if force_objective_constant or (constant != 0.0):
                    # ONE_VAR_CONSTANT
                    column_data[-1].append((objective_label, constant))

        if numObj == 0:
            raise ValueError(
                "Cannot write legal MPS file: No objective defined "
                "for input model '%s'." % str(model)
            )
        assert objective_label is not None

        # Constraints
        def constraint_generator():
            for block in all_blocks:
                gen_con_repn = getattr(block, "_gen_con_repn", True)

                # Get/Create the ComponentMap for the repn
                if not hasattr(block, '_repn'):
                    block._repn = ComponentMap()
                block_repn = block._repn

                for constraint_data in block.component_data_objects(
                    Constraint, active=True, sort=sortOrder, descend_into=False
                ):
                    if (not constraint_data.has_lb()) and (
                        not constraint_data.has_ub()
                    ):
                        assert not constraint_data.equality
                        continue  # non-binding, so skip

                    if constraint_data._linear_canonical_form:
                        repn = constraint_data.canonical_form()
                    elif gen_con_repn:
                        repn = generate_standard_repn(constraint_data.body)
                        block_repn[constraint_data] = repn
                    else:
                        repn = block_repn[constraint_data]

                    yield constraint_data, repn

        if row_order is not None:
            sorted_constraint_list = list(constraint_generator())
            sorted_constraint_list.sort(key=lambda x: row_order[x[0]])

            def yield_all_constraints():
                for constraint_data, repn in sorted_constraint_list:
                    yield constraint_data, repn

        else:
            yield_all_constraints = constraint_generator

        for constraint_data, repn in yield_all_constraints():
            degree = repn.polynomial_degree()

            # Write constraint
            if degree == 0:
                if skip_trivial_constraints:
                    continue
            elif degree is None:
                raise RuntimeError(
                    "Cannot write legal MPS file. Constraint '%s' "
                    "has nonlinear terms that are not quadratic." % constraint_data.name
                )

            # Create symbol
            con_symbol = create_symbol_func(symbol_map, constraint_data, labeler)

            if constraint_data.equality:
                assert value(constraint_data.lower) == value(constraint_data.upper)
                label = 'c_e_' + con_symbol + '_'
                alias_symbol_func(symbol_map, constraint_data, label)
                output_file.write(" E  %s\n" % (label))
                offset = extract_variable_coefficients(
                    label, repn, column_data, quadmatrix_data, variable_to_column
                )
                bound = constraint_data.lower
                bound = _get_bound(bound) - offset
                rhs_data.append((label, _no_negative_zero(bound)))
            else:
                if constraint_data.has_lb():
                    if constraint_data.has_ub():
                        label = 'r_l_' + con_symbol + '_'
                    else:
                        label = 'c_l_' + con_symbol + '_'
                    alias_symbol_func(symbol_map, constraint_data, label)
                    output_file.write(" G  %s\n" % (label))
                    offset = extract_variable_coefficients(
                        label, repn, column_data, quadmatrix_data, variable_to_column
                    )
                    bound = constraint_data.lower
                    bound = _get_bound(bound) - offset
                    rhs_data.append((label, _no_negative_zero(bound)))
                else:
                    assert constraint_data.has_ub()

                if constraint_data.has_ub():
                    if constraint_data.has_lb():
                        label = 'r_u_' + con_symbol + '_'
                    else:
                        label = 'c_u_' + con_symbol + '_'
                    alias_symbol_func(symbol_map, constraint_data, label)
                    output_file.write(" L  %s\n" % (label))
                    offset = extract_variable_coefficients(
                        label, repn, column_data, quadmatrix_data, variable_to_column
                    )
                    bound = constraint_data.upper
                    bound = _get_bound(bound) - offset
                    rhs_data.append((label, _no_negative_zero(bound)))
                else:
                    assert constraint_data.has_lb()

        if len(column_data[-1]) > 0:
            # ONE_VAR_CONSTANT = 1
            output_file.write(" E  c_e_ONE_VAR_CONSTANT\n")
            column_data[-1].append(("c_e_ONE_VAR_CONSTANT", 1))
            rhs_data.append(("c_e_ONE_VAR_CONSTANT", 1))

        #
        # COLUMNS section
        #
        column_template = "     %s %s %" + self._precision_string + "\n"
        output_file.write("COLUMNS\n")
        cnt = 0
        in_integer_section = False
        mark_cnt = 0
        for vardata in variable_list:
            col_entries = column_data[variable_to_column[vardata]]
            cnt += 1
            if len(col_entries) > 0:
                if self._int_marker:
                    if vardata.is_integer():
                        if not in_integer_section:
                            output_file.write(
                                f"     MARK{mark_cnt:04d} 'MARKER' 'INTORG'\n"
                            )
                            in_integer_section = True
                            mark_cnt += 1
                    elif in_integer_section:  # and not vardata.is_integer()
                        output_file.write(
                            f"     MARK{mark_cnt:04d} 'MARKER' 'INTEND'\n"
                        )
                        in_integer_section = False
                        mark_cnt += 1

                var_label = variable_symbol_dictionary[id(vardata)]
                for i, (row_label, coef) in enumerate(col_entries):
                    output_file.write(
                        column_template
                        % (var_label, row_label, _no_negative_zero(coef))
                    )
            elif include_all_variable_bounds:
                # the column is empty, so add a (0 * var)
                # term to the objective
                # * Note that some solvers (e.g., Gurobi)
                #   will accept an empty column as a line
                #   with just the column name. This doesn't
                #   seem to work for CPLEX 12.6, so I am
                #   doing it this way so that it will work for both
                var_label = variable_symbol_dictionary[id(vardata)]
                output_file.write(column_template % (var_label, objective_label, 0))

        if self._int_marker and in_integer_section:
            output_file.write(f"     MARK{mark_cnt:04d} 'MARKER' 'INTEND'\n")

        assert cnt == len(column_data) - 1
        if len(column_data[-1]) > 0:
            col_entries = column_data[-1]
            var_label = "ONE_VAR_CONSTANT"
            for i, (row_label, coef) in enumerate(col_entries):
                output_file.write(
                    column_template % (var_label, row_label, _no_negative_zero(coef))
                )

        #
        # RHS section
        #
        rhs_template = "     RHS %s %" + self._precision_string + "\n"
        output_file.write("RHS\n")
        for i, (row_label, rhs) in enumerate(rhs_data):
            # note: we have already converted any -0 to 0 by this point
            output_file.write(rhs_template % (row_label, rhs))

        # SOS constraints
        SOSlines = StringIO()
        sos1 = solver_capability("sos1")
        sos2 = solver_capability("sos2")
        for block in all_blocks:
            for soscondata in block.component_data_objects(
                SOSConstraint, active=True, sort=sortOrder, descend_into=False
            ):
                create_symbol_func(symbol_map, soscondata, labeler)

                level = soscondata.level
                if (
                    (level == 1 and not sos1)
                    or (level == 2 and not sos2)
                    or (level > 2)
                ):
                    raise ValueError(
                        "Solver does not support SOS level %s constraints" % (level)
                    )
                # This updates the referenced_variable_ids, just in case
                # there is a variable that only appears in an
                # SOSConstraint, in which case this needs to be known
                # before we write the "bounds" section (Cplex does not
                # handle this correctly, Gurobi does)
                self._printSOS(
                    symbol_map, labeler, variable_symbol_map, soscondata, SOSlines
                )

        #
        # BOUNDS section
        #
        entry_template = "%s %" + self._precision_string + "\n"
        output_file.write("BOUNDS\n")
        for vardata in variable_list:
            if include_all_variable_bounds or (
                id(vardata) in self._referenced_variable_ids
            ):
                var_label = variable_symbol_dictionary[id(vardata)]
                if vardata.fixed:
                    if not output_fixed_variable_bounds:
                        raise ValueError(
                            "Encountered a fixed variable (%s) inside an active "
                            "objective or constraint expression on model %s, which is "
                            "usually indicative of a preprocessing error. Use the "
                            "IO-option 'output_fixed_variable_bounds=True' to suppress "
                            "this error and fix the variable by overwriting its bounds "
                            "in the MPS file." % (vardata.name, model.name)
                        )
                    if vardata.value is None:
                        raise ValueError("Variable cannot be fixed to a value of None.")
                    output_file.write(
                        (" FX BOUND " + entry_template)
                        % (var_label, _no_negative_zero(value(vardata.value)))
                    )
                    continue

                # convert any -0 to 0 to make baseline diffing easier
                vardata_lb = _no_negative_zero(_get_bound(vardata.lb))
                vardata_ub = _no_negative_zero(_get_bound(vardata.ub))
                unbounded_lb = not vardata.has_lb()
                unbounded_ub = not vardata.has_ub()
                treat_as_integer = False
                if vardata.is_binary():
                    if (vardata_lb == 0) and (vardata_ub == 1):
                        output_file.write(" BV BOUND %s\n" % (var_label))
                        continue
                    else:
                        # so we can add bounds
                        treat_as_integer = True
                if treat_as_integer or vardata.is_integer():
                    # Indicating unbounded integers is tricky because
                    # the only way to indicate a variable is integer
                    # is using the bounds section. Thus, we signify
                    # infinity with a large number (10E20)
                    # * Note: Gurobi allows values like inf and -inf
                    #         but CPLEX 12.6 does not, so I am just
                    #         using a large value
                    if not unbounded_lb:
                        output_file.write(
                            (" LI BOUND " + entry_template) % (var_label, vardata_lb)
                        )
                    else:
                        output_file.write(" LI BOUND %s -10E20\n" % (var_label))
                    if not unbounded_ub:
                        output_file.write(
                            (" UI BOUND " + entry_template) % (var_label, vardata_ub)
                        )
                    else:
                        output_file.write(" UI BOUND %s 10E20\n" % (var_label))
                else:
                    assert vardata.is_continuous()
                    if unbounded_lb and unbounded_ub:
                        output_file.write(" FR BOUND %s\n" % (var_label))
                    else:
                        if not unbounded_lb:
                            output_file.write(
                                (" LO BOUND " + entry_template)
                                % (var_label, vardata_lb)
                            )
                        else:
                            output_file.write(" MI BOUND %s\n" % (var_label))

                        if not unbounded_ub:
                            output_file.write(
                                (" UP BOUND " + entry_template)
                                % (var_label, vardata_ub)
                            )

        #
        # SOS section
        #
        output_file.write(SOSlines.getvalue())

        # Formatting of the next two sections comes from looking
        # at Gurobi and Cplex output

        #
        # QUADOBJ section
        #
        if len(quadobj_data) > 0:
            assert len(quadobj_data) == 1
            # it looks like the COIN-OR MPS Reader only
            # recognizes QUADOBJ (Gurobi and Cplex seem to
            # be okay with this)
            output_file.write("QUADOBJ\n")
            # output_file.write("QMATRIX\n")
            label, quad_terms = quadobj_data[0]
            assert label == objective_label
            # sort by the sorted tuple of symbols (or column assignments)
            # for the variables appearing in the term
            quad_terms = sorted(
                quad_terms,
                key=lambda _x: sorted(
                    (variable_to_column[_x[0][0]], variable_to_column[_x[0][1]])
                ),
            )
            for term, coef in quad_terms:
                # sort the term for consistent output
                var1, var2 = sorted(term, key=lambda _x: variable_to_column[_x])
                var1_label = variable_symbol_dictionary[id(var1)]
                var2_label = variable_symbol_dictionary[id(var2)]
                # Don't forget that a quadratic objective is always
                # assumed to be divided by 2
                if var1_label == var2_label:
                    output_file.write(
                        column_template
                        % (var1_label, var2_label, _no_negative_zero(coef * 2))
                    )
                else:
                    # The Q matrix is symmetric (but remember it is divided by 2).
                    # For the QUADOBJ section we output only the upper (or lower)
                    # triangle of the Q matrix, so a single element represents a pair
                    # of non-zero elements.
                    output_file.write(
                        column_template
                        % (var1_label, var2_label, _no_negative_zero(coef))
                    )

        #
        # QCMATRIX section
        #
        if len(quadmatrix_data) > 0:
            for row_label, quad_terms in quadmatrix_data:
                output_file.write("QCMATRIX    %s\n" % (row_label))
                # sort by the sorted tuple of symbols (or
                # column assignments) for the variables
                # appearing in the term
                quad_terms = sorted(
                    quad_terms,
                    key=lambda _x: sorted(
                        (variable_to_column[_x[0][0]], variable_to_column[_x[0][1]])
                    ),
                )
                for term, coef in quad_terms:
                    # sort the term for consistent output
                    var1, var2 = sorted(term, key=lambda _x: variable_to_column[_x])
                    var1_label = variable_symbol_dictionary[id(var1)]
                    var2_label = variable_symbol_dictionary[id(var2)]
                    if var1_label == var2_label:
                        output_file.write(
                            column_template
                            % (var1_label, var2_label, _no_negative_zero(coef))
                        )
                    else:
                        # the matrix needs to be symmetric so split
                        # the coefficient
                        output_file.write(
                            column_template
                            % (var1_label, var2_label, _no_negative_zero(coef * 0.5))
                        )
                        output_file.write(
                            column_template % (var2_label, var1_label, coef * 0.5)
                        )

        output_file.write("ENDATA\n")

        # Clean up the symbol map to only contain variables referenced
        # in the active constraints **Note**: warm start method may
        # rely on this for choosing the set of potential warm start
        # variables
        vars_to_delete = set(variable_symbol_map.byObject.keys()) - set(
            self._referenced_variable_ids.keys()
        )
        sm_byObject = symbol_map.byObject
        sm_bySymbol = symbol_map.bySymbol
        var_sm_byObject = variable_symbol_map.byObject
        for varid in vars_to_delete:
            symbol = var_sm_byObject[varid]
            del sm_byObject[varid]
            del sm_bySymbol[symbol]
        del variable_symbol_map

        return symbol_map
