ommx_pyscipopt_adapter
======================

.. py:module:: ommx_pyscipopt_adapter


Submodules
----------

.. toctree::
   :maxdepth: 1

   /autoapi/ommx_pyscipopt_adapter/adapter/index
   /autoapi/ommx_pyscipopt_adapter/exception/index


Exceptions
----------

.. autoapisummary::

   ommx_pyscipopt_adapter.OMMXPySCIPOptAdapterError


Classes
-------

.. autoapisummary::

   ommx_pyscipopt_adapter.OMMXPySCIPOptAdapter
   ommx_pyscipopt_adapter.SCIPDiagnosticsAnalyzer
   ommx_pyscipopt_adapter.SCIPProgressSnapshot
   ommx_pyscipopt_adapter.SCIPTerminationReport


Package Contents
----------------

.. py:exception:: OMMXPySCIPOptAdapterError



   Common base class for all non-exit exceptions.


.. py:class:: OMMXPySCIPOptAdapter(ommx_instance: ommx.Instance, *, initial_state: Optional[ommx.ToState] = None)



   An abstract interface for OMMX Solver Adapters, defining how solvers should be used with OMMX.

   See the `implementation guide <https://jij-inc-ommx.readthedocs-hosted.com/en/latest/tutorial/implement_adapter.html>`_ for more details.

   Concrete subclasses define applicability with ``INPUT_CLASS``. The easy
   :meth:`solve` API prepares an isolated copy with the Adapter's recommended
   policy. Use :meth:`solve_without_preparation` when the caller owns preparation and wants
   the Adapter to require an exact input without modifying it.


   .. py:method:: check_applicability(ommx_instance: ommx.Instance) -> ommx.InstanceClassMembershipReport
      :classmethod:


      Check ``INPUT_CLASS`` membership without mutation.



   .. py:method:: decode(data: pyscipopt.Model) -> ommx.Solution

      Convert optimized pyscipopt.Model and ommx.Instance to ommx.Solution.

      This method is intended to be used if the model has been acquired with
      `solver_input` for further adjustment of the solver parameters, and
      separately optimizing the model.

      Note that alterations to the model may make the decoding process
      incompatible -- decoding will only work if the model still describes
      effectively the same problem as the OMMX instance used to create the
      adapter.

      Backend optimality is mapped through the instance's output-objective
      semantics. It remains unspecified when active-formulation optimality
      does not transport to that objective.

      Examples
      =========

      .. doctest::

          >>> from ommx_pyscipopt_adapter import OMMXPySCIPOptAdapter
          >>> from ommx import Instance, DecisionVariable

          >>> p = [10, 13, 18, 32, 7, 15]
          >>> w = [11, 15, 20, 35, 10, 33]
          >>> x = [DecisionVariable.binary(i) for i in range(6)]
          >>> instance = Instance.from_components(
          ...     decision_variables=x,
          ...     objective=sum(p[i] * x[i] for i in range(6)),
          ...     constraints={0: sum(w[i] * x[i] for i in range(6)) <= 47},
          ...     sense=Instance.MAXIMIZE,
          ... )

          >>> adapter = OMMXPySCIPOptAdapter(instance)
          >>> model = adapter.solver_input
          >>> # ... some modification of model's parameters
          >>> model.optimize()

          >>> solution = adapter.decode(model)
          >>> solution.objective
          42.0




   .. py:method:: decode_to_state(data: pyscipopt.Model) -> ommx.State

      Create an ommx.State from an optimized PySCIPOpt Model.

      Examples
      =========

      .. doctest::

          The following example shows how to solve an unconstrained linear optimization problem with `x1` as the objective function.

          >>> from ommx_pyscipopt_adapter import OMMXPySCIPOptAdapter
          >>> from ommx import Instance, DecisionVariable

          >>> x1 = DecisionVariable.integer(1, lower=0, upper=5)
          >>> ommx_instance = Instance.from_components(
          ...     decision_variables=[x1],
          ...     objective=x1,
          ...     constraints={},
          ...     sense=Instance.MINIMIZE,
          ... )
          >>> adapter = OMMXPySCIPOptAdapter(ommx_instance)
          >>> model = adapter.solver_input
          >>> model.optimize()

          >>> ommx_state = adapter.decode_to_state(model)
          >>> ommx_state.entries
          {1: 0.0}




   .. py:method:: recommended_preparation_policy() -> ommx.PreparationPolicy
      :classmethod:


      Recommend lowering OneHot constraints before using PySCIPOpt.

      PySCIPOpt accepts Indicator and SOS1 constraints directly, so this
      recommendation preserves those families and lowers only OneHot
      constraints. The easy API applies a fresh policy to an isolated copy;
      callers may instead edit and apply one before invoking
      :meth:`solve_without_preparation`.



   .. py:method:: require_applicable(ommx_instance: ommx.Instance) -> ommx.InstanceClassMembershipReport
      :classmethod:


      Return the membership report or raise ``AdapterNotApplicableError``.



   .. py:method:: solve(ommx_instance: ommx.Instance, *, initial_state: Optional[ommx.ToState] = None, diagnostics: ommx.adapter.DiagnosticsSink | None = None) -> ommx.Solution
      :classmethod:


      Solve the given ommx.Instance using PySCIPopt, returning an ommx.Solution.

      The input instance is not modified. An isolated copy is prepared with
      the recommended PySCIPOpt policy before preparation-free execution.

      :param ommx_instance: The ommx.Instance to prepare and solve.
      :param initial_state: Optional initial solution state.

      Examples
      =========

      KnapSack Problem

      .. doctest::

          >>> from ommx import Instance, DecisionVariable
          >>> from ommx import Solution
          >>> from ommx_pyscipopt_adapter import OMMXPySCIPOptAdapter

          >>> p = [10, 13, 18, 32, 7, 15]
          >>> w = [11, 15, 20, 35, 10, 33]
          >>> x = [DecisionVariable.binary(i) for i in range(6)]
          >>> instance = Instance.from_components(
          ...     decision_variables=x,
          ...     objective=sum(p[i] * x[i] for i in range(6)),
          ...     constraints={0: sum(w[i] * x[i] for i in range(6)) <= 47},
          ...     sense=Instance.MAXIMIZE,
          ... )

          Solve it

          >>> solution = OMMXPySCIPOptAdapter.solve(instance)

          Check output

          >>> sorted([(id, value) for id, value in solution.state.entries.items()])
          [(0, 1.0), (1, 0.0), (2, 0.0), (3, 1.0), (4, 0.0), (5, 0.0)]
          >>> solution.feasible
          True
          >>> assert solution.optimality == Solution.OPTIMAL

          p[0] + p[3] = 42
          w[0] + w[3] = 46 <= 47

          >>> solution.objective
          42.0
          >>> solution.get_constraint_value(0)
          -1.0

      Infeasible Problem

      .. doctest::

              >>> from ommx import Instance, DecisionVariable
              >>> from ommx_pyscipopt_adapter import OMMXPySCIPOptAdapter

              >>> x = DecisionVariable.integer(0, upper=3, lower=0)
              >>> instance = Instance.from_components(
              ...     decision_variables=[x],
              ...     objective=x,
              ...     constraints={0: x >= 4},
              ...     sense=Instance.MAXIMIZE,
              ... )

              >>> OMMXPySCIPOptAdapter.solve(instance)
              Traceback (most recent call last):
                  ...
              ommx.InfeasibleDetected: Model was infeasible

      Unbounded Problem

      .. doctest::

              >>> from ommx import Instance, DecisionVariable
              >>> from ommx_pyscipopt_adapter import OMMXPySCIPOptAdapter

              >>> x = DecisionVariable.integer(0, lower=0)
              >>> instance = Instance.from_components(
              ...     decision_variables=[x],
              ...     objective=x,
              ...     constraints={},
              ...     sense=Instance.MAXIMIZE,
              ... )

              >>> OMMXPySCIPOptAdapter.solve(instance)
              Traceback (most recent call last):
                  ...
              ommx.adapter.UnboundedDetected: Model was unbounded



   .. py:method:: solve_without_preparation(ommx_instance: ommx.Instance, *, initial_state: Optional[ommx.ToState] = None, diagnostics: ommx.adapter.DiagnosticsSink | None = None) -> ommx.Solution
      :classmethod:


      Solve an exact PySCIPOpt Adapter input without preparing it.



   .. py:attribute:: INPUT_CLASS
      :type:  ClassVar[ommx.InstanceClass]

      Required condition for an exact Adapter input.



   .. py:property:: solver_input
      :type: pyscipopt.Model


      The PySCIPOpt model generated from this OMMX instance



.. py:class:: SCIPDiagnosticsAnalyzer(diagnostics: Iterable[Any])

   Pandas-like post-processor for PySCIPOpt diagnostics.

   The analyzer accepts either typed diagnostics collected by
   :class:`ommx.adapter.DiagnosticCollector` or dictionaries loaded from
   :attr:`ommx.experiment.Solve.diagnostics`.

   :attr:`progress_history_records` returns ``list[dict[str, object]]`` and
   does not require pandas. When diagnostics include a terminal SCIP report,
   the progress history includes a final ``TERMINATION`` row derived from that
   report. :attr:`progress_history_df` returns a pandas DataFrame indexed by
   ``solving_time_sec`` and imports pandas lazily. Time-series properties such
   as :attr:`dual_bound` return pandas Series with the same index.
   :attr:`termination_result` returns the terminal SCIP report as a dictionary.


   .. py:property:: dual_bound
      :type: Any


      Return dual bounds indexed by solving time.



   .. py:property:: event
      :type: Any


      Return progress event names indexed by solving time.



   .. py:property:: gap
      :type: Any


      Return relative gaps indexed by solving time.



   .. py:property:: incumbent_objective
      :type: Any


      Return incumbent objectives indexed by solving time.



   .. py:property:: lp_iteration_count
      :type: Any


      Return LP iteration counts indexed by solving time.



   .. py:property:: node_count
      :type: Any


      Return processed node counts indexed by solving time.



   .. py:property:: primal_bound
      :type: Any


      Return primal bounds indexed by solving time.



   .. py:property:: progress_history_df
      :type: Any


      Return progress history as a pandas DataFrame indexed by time.



   .. py:property:: progress_history_records
      :type: list[dict[str, object]]


      Return SCIP progress history, with ``TERMINATION`` when present.



   .. py:property:: progress_snapshots
      :type: tuple[SCIPProgressSnapshot, ...]


      Typed progress snapshots in the order they were recorded.



   .. py:property:: solution_count
      :type: Any


      Return stored solution counts indexed by solving time.



   .. py:property:: termination_result
      :type: dict[str, object] | None


      Return the terminal SCIP report as one dictionary, if present.



   .. py:property:: total_node_count
      :type: Any


      Return total processed node counts indexed by solving time.



.. py:class:: SCIPProgressSnapshot

   SCIP solve progress observed during a solve.

   The PySCIPOpt adapter records this snapshot for each tracked SCIP event and
   once more from the termination report after ``model.optimize()`` finishes.
   It currently listens for ``BESTSOLFOUND`` and ``DUALBOUNDIMPROVED``. Event
   snapshots are the model state visible from that callback. SCIP may call a
   ``BESTSOLFOUND`` callback before every aggregate model statistic has been
   updated, so use the ``TERMINATION`` snapshot or
   :class:`SCIPTerminationReport` for terminal values.


   .. py:method:: from_event(model: pyscipopt.Model, event: pyscipopt.scip.Event) -> SCIPProgressSnapshot
      :classmethod:



   .. py:method:: from_termination_report(report: SCIPTerminationReport) -> SCIPProgressSnapshot
      :classmethod:



   .. py:attribute:: dual_bound
      :type:  float

      SCIP dual bound reported at the snapshot.



   .. py:attribute:: event
      :type:  str

      Progress marker.

      Callback snapshots use SCIP event names such as ``"BESTSOLFOUND"`` and
      ``"DUALBOUNDIMPROVED"``. The terminal snapshot uses the synthetic
      ``"TERMINATION"`` marker.



   .. py:attribute:: gap
      :type:  float

      SCIP relative gap reported at the snapshot.



   .. py:attribute:: incumbent_objective
      :type:  float | None

      Objective value of SCIP's current best solution.

      This is ``None`` when PySCIPOpt cannot read an incumbent objective at that
      snapshot.



   .. py:attribute:: lp_iteration_count
      :type:  int

      LP iterations at the snapshot.



   .. py:attribute:: node_count
      :type:  int

      Processed branch-and-bound nodes at the snapshot.



   .. py:attribute:: primal_bound
      :type:  float

      SCIP primal bound reported at the snapshot.



   .. py:attribute:: solution_count
      :type:  int

      Number of solutions stored by SCIP at the snapshot.



   .. py:attribute:: solving_time_sec
      :type:  float

      SCIP solving time when the snapshot was recorded.



   .. py:attribute:: total_node_count
      :type:  int

      Total processed nodes including restarts at the snapshot.



.. py:class:: SCIPTerminationReport

   SCIP-side termination summary recorded after ``model.optimize()``.

   The PySCIPOpt adapter records this report before decoding the optimized
   SCIP model back into an OMMX solution. It is therefore available even when
   decoding raises an adapter exception such as infeasible or unbounded
   detection.


   .. py:method:: from_model(model: pyscipopt.Model) -> SCIPTerminationReport
      :classmethod:



   .. py:attribute:: applied_cut_count
      :type:  int

      Number of cuts applied by SCIP.



   .. py:attribute:: best_solution_count
      :type:  int

      Number of new incumbent solutions SCIP found.



   .. py:attribute:: cut_count
      :type:  int

      Number of cuts available in SCIP's cut pool.



   .. py:attribute:: dual_bound
      :type:  float

      SCIP dual bound at termination.



   .. py:attribute:: gap
      :type:  float

      SCIP relative gap reported by ``getGap()``.



   .. py:attribute:: lp_iteration_count
      :type:  int

      Total LP iterations.



   .. py:attribute:: lp_solve_count
      :type:  int

      Number of solved LPs.



   .. py:attribute:: max_depth
      :type:  int

      Maximum branch-and-bound depth.

      SCIP may report ``-1`` when no branching occurred.



   .. py:attribute:: node_count
      :type:  int

      Number of branch-and-bound nodes processed by SCIP.



   .. py:attribute:: objective_value
      :type:  float | None

      Incumbent objective value, or ``None`` when SCIP has no solution.



   .. py:attribute:: presolving_time_sec
      :type:  float

      SCIP presolving time in seconds.



   .. py:attribute:: primal_bound
      :type:  float

      SCIP primal bound at termination.



   .. py:attribute:: primal_dual_integral
      :type:  float

      SCIP primal-dual integral at termination.



   .. py:attribute:: pyscipopt_version
      :type:  str | None

      PySCIPOpt package version, if available.



   .. py:attribute:: reading_time_sec
      :type:  float

      SCIP reading time in seconds.



   .. py:attribute:: scip_version
      :type:  str

      SCIP version used through PySCIPOpt.



   .. py:attribute:: solution_count
      :type:  int

      Number of solutions stored by SCIP at termination.



   .. py:attribute:: solution_found_count
      :type:  int

      Number of solutions SCIP found during the solve.



   .. py:attribute:: solving_time_sec
      :type:  float

      SCIP solving time in seconds.



   .. py:attribute:: status
      :type:  str

      SCIP termination status, such as ``"optimal"``, ``"infeasible"``, or
      ``"unbounded"``.



   .. py:attribute:: total_node_count
      :type:  int

      Total processed nodes including restarts.



