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.

   Subclasses should set ``ADDITIONAL_CAPABILITIES`` to declare which non-standard
   constraint types they can handle. Standard constraints are always supported.

   Available capabilities:

   - ``AdditionalCapability.Indicator``: binvar = 1 → f(x) <= 0
   - ``AdditionalCapability.OneHot``: exactly one of a set of binary variables is 1
   - ``AdditionalCapability.Sos1``: at most one of a set of variables is non-zero

   The default is an empty set (standard constraints only).
   Subclasses must call ``super().__init__(ommx_instance)`` so that any
   constraint types the adapter does not support are automatically converted
   into regular constraints (Big-M for indicator / SOS1, linear equality for
   one-hot). Conversions mutate ``ommx_instance`` in place and are emitted
   at ``INFO`` level as ``tracing`` events from the Rust SDK; configure a
   Python OpenTelemetry ``TracerProvider`` before the first call to observe
   them via ``pyo3-tracing-opentelemetry``.


   .. 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.

      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:: 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.

      :param ommx_instance: The ommx.Instance to 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.adapter.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:attribute:: ADDITIONAL_CAPABILITIES


   .. 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, Ellipsis]


      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.



