.. _dirac-3s:

=========================
Dirac-3S (dirac-3s)
=========================

``dirac-3s`` is QCi's second generation Dirac-3 device. It runs the same two qudit problem types as
``dirac-3``, namely the normalized-qudit (continuous) and qudit (integer) Hamiltonian
optimizations, and is selected by name in the job parameters.


Selecting the Dirac-3S Device
==================================

Pass ``device_type="dirac-3s"`` in the ``job_params`` dictionary given to
``build_job_body``---

.. code-block:: python

  job_body = client.build_job_body(
      job_type="sample-hamiltonian",
      job_params={"device_type": "dirac-3s", "sum_constraint": 1},
      polynomial_file_id=file_id,
  )

The ``device_type`` key is required for every job type. Omitting it raises---

.. code-block:: text

  ValueError: no 'device_type' specified in job_params, must be one of ('dirac-1',
  'dirac-3', 'dirac-3s', 'dirac-3_normalized_qudit', 'dirac-3_qudit')

An unrecognized device name raises a :class:`ValueError` from
:class:`qci_client.optimization.enum.DeviceType`.


Supported Job Types on Dirac-3S
====================================

``dirac-3s`` accepts both qudit job types.

.. list-table::
  :header-rows: 1
  :widths: 25 40 35

  * - ``job_type``
    - ``problem_config`` key
    - Required file ID
  * - ``sample-hamiltonian``
    - ``normalized_qudit_hamiltonian_optimization``
    - exactly one of ``polynomial_file_id`` or ``hamiltonian_file_id``
  * - ``sample-hamiltonian-integer``
    - ``qudit_hamiltonian_optimization``
    - exactly one of ``polynomial_file_id`` or ``hamiltonian_file_id``

Use ``sample-hamiltonian`` for continuous variables subject to a sum constraint, and
``sample-hamiltonian-integer`` for integer variables with a bounded number of levels per
variable.

Supplying both ``polynomial_file_id`` and ``hamiltonian_file_id``, or neither, raises---

.. code-block:: text

  AssertionError: exactly one of hamiltonian_file_id or polynomial_file_id must be
  specified for job_type='<job_type>'

.. note::
  ``hamiltonian_file_id`` is deprecated for the Hamiltonian job types. Prefer
  ``polynomial_file_id``.

Every other job type is restricted to qubit devices. Pairing one with ``dirac-3s``
raises ``ValueError: <job_type> not supported on dirac-3s``.


Device-Name Remapping and Job Metrics
=====================================

``dirac-3`` is a convenience alias. When it is requested, the client rewrites the
``device_config`` key to the specific device name that the API expects, based on the job
type. ``dirac-3s`` is passed through literally and is never rewritten.

.. list-table::
  :header-rows: 1
  :widths: 25 40 35

  * - Requested ``device_type``
    - ``job_type``
    - Submitted ``device_config`` key
  * - ``dirac-3``
    - ``sample-hamiltonian``
    - ``dirac-3_normalized_qudit``
  * - ``dirac-3``
    - ``sample-hamiltonian-integer``
    - ``dirac-3_qudit``
  * - ``dirac-3s``
    - either
    - ``dirac-3s``

So a job body built for ``dirac-3s`` keys its device configuration exactly as
written---

.. code-block:: json

  {
    "job_submission": {
      "problem_config": {
        "normalized_qudit_hamiltonian_optimization": {
          "polynomial_file_id": "<file id>"
        }
      },
      "device_config": {
        "dirac-3s": {
          "num_samples": 5,
          "relaxation_schedule": 1,
          "sum_constraint": 10
        }
      }
    }
  }

This matters when reading job metrics, which are keyed by the device name that was
actually submitted rather than the one that was requested. Read the device block without
hardcoding the name---

.. code-block:: python

  metrics = client.get_job_metrics(job_id=job_id)
  device = next(iter(metrics["job_metrics"]["time_ns"]["device"].values()))


Dirac-3S Device Configuration Parameters
=============================================

``job_params`` is a flat dictionary. ``build_job_body`` sorts its keys into
``problem_config`` or ``device_config``. The parameters below are the ones routed to
``device_config`` for the two job types that ``dirac-3s`` supports.

``num_samples``
---------------

How many samples the stochastic solver draws.

- Range: 1 to 100, inclusive.
- Default: 1.
- Accepted by both job types.

``relaxation_schedule``
-----------------------

Tunes a group of device parameters as a preset. Higher schedules give better solution
quality at the cost of longer evolution time.

- Range: 1 to 4, inclusive.
- Default: 1.
- Accepted by both job types.

``sum_constraint``
------------------

The value that the solution variables must sum to. This is what makes a normalized-qudit
problem normalized: the solution lives on a simplex of the given size.

- Range: 1 to 10000, inclusive.
- Accepted by ``sample-hamiltonian`` only.

``num_levels``
--------------

The number of discrete levels available to each variable, as a list of integers. A
variable with ``k`` levels ranges over ``0`` through ``k - 1``.

- Required by ``sample-hamiltonian-integer``. Omitting it raises
  ``AssertionError: num_levels is a required field``.

``mean_photon_number``
----------------------

Advanced. Overrides the value that ``relaxation_schedule`` would otherwise set
implicitly.

- Range: 0.0000667 to 0.0066666, inclusive.
- Accepted by both job types.

``quantum_fluctuation_coefficient``
-----------------------------------

Advanced. Also overrides a value implied by ``relaxation_schedule``.

- Range: 1 to 100, inclusive.
- Accepted by both job types.

.. note::
  The client validates that parameter names are recognized and that the job type and
  device are compatible. It does **not** range-check numeric values. The ranges
  above are enforced by the API, which rejects out-of-range submissions with an
  :class:`requests.HTTPError`. Unrecognized keys are dropped from the job body
  without warning, so inspect the built body when a parameter appears to have no
  effect.

.. warning::
  The ``solution_precision`` parameter is no longer supported. Passing it to
  ``build_job_body`` emits a :class:`UserWarning` and the value is ignored; use
  client-side postprocessing to distill continuous solutions instead.


Continuous Example on Dirac-3S
===================================

This minimizes the polynomial ``H = -x1^2 + 2*x1*x2 - x2^2`` subject to ``x1 + x2 = 1``
with both variables non-negative. The constraint is expressed through
``sum_constraint`` rather than through a constraints file, which is what the
normalized-qudit problem type is for. The optimum is ``H = -1``, reached when one
variable takes the whole budget and the other is zero.

In the polynomial file, each entry of ``data`` is one term: ``idx`` names the
variables in that term and ``val`` is its coefficient. Variables are numbered from
1, so ``[1, 1]`` is ``x1^2`` and ``[1, 2]`` is ``x1*x2``. Every ``idx`` list has
length ``max_degree``, and all three terms here are of degree 2.

.. code-block:: python

  from qci_client import QciClient

  client = QciClient()

  polynomial = {
      "file_name": "dirac-3s-continuous-example",
      "file_config": {
          "polynomial": {
              "num_variables": 2,
              "min_degree": 2,
              "max_degree": 2,
              "data": [
                  {"idx": [1, 1], "val": -1.0},
                  {"idx": [1, 2], "val": 2.0},
                  {"idx": [2, 2], "val": -1.0},
              ],
          }
      },
  }

  file_id = client.upload_file(file=polynomial)["file_id"]

  job_body = client.build_job_body(
      job_type="sample-hamiltonian",
      job_name="dirac-3s-continuous-example",
      job_tags=["quickstart"],
      job_params={
          "device_type": "dirac-3s",
          "relaxation_schedule": 1,
          "sum_constraint": 1,
      },
      polynomial_file_id=file_id,
  )

  response = client.process_job(job_body=job_body)

  if response["status"] != "COMPLETED":
      raise RuntimeError(f"job did not complete: {response['status']}")

  print(f"x = {response['results']['solutions'][0]}")
  print(f"H = {response['results']['energies'][0]}")

``process_job`` blocks until the job reaches a terminal status and, because ``verbose``
defaults to ``True``, logs its progress---

.. code-block:: text

  2026-08-17 10:14:02 - Dirac allocation balance = 600.0 s
  2026-08-17 10:14:03 - Job submitted: job_id='6534d9d1e4b0a1f2c3d40001'
  2026-08-17 10:14:03 - QUEUED
  2026-08-17 10:14:11 - RUNNING
  2026-08-17 10:14:19 - COMPLETED
  2026-08-17 10:14:19 - Dirac allocation balance = 599.0 s
  x = [1.0, 0.0]
  H = -1.0


Integer Example on Dirac-3S
================================

This runs an integer problem on ``dirac-3s`` and drives the polling loop by hand
instead of using ``process_job``. The pattern is useful when other work should
proceed while the job runs, or when a failed job is to be handled without an
exception.

The polynomial spans degrees 2 through 4 over two variables, so every ``idx`` list
has length 4 and lower-degree terms are left-padded with ``0``. The index ``0`` is
padding, not a variable: ``[0, 0, 1, 1]`` is ``x1^2``, ``[0, 1, 1, 1]`` is ``x1^3``,
and ``[1, 1, 1, 1]`` is ``x1^4``. Indices must be non-decreasing from left to right,
so each term has exactly one representation.

The ``num_levels`` entry gives one level count per variable and is required for integer
jobs.

.. code-block:: python

  from qci_client import QciClient, JobStatus, JOB_STATUSES_FINAL

  client = QciClient()

  polynomial = {
      "file_name": "dirac-3s-integer-example",
      "file_config": {
          "polynomial": {
              "num_variables": 2,
              "min_degree": 2,
              "max_degree": 4,
              "data": [
                  {"idx": [0, 0, 1, 1], "val": 1.0},
                  {"idx": [0, 1, 1, 1], "val": -2.0},
                  {"idx": [1, 1, 1, 1], "val": 1.0},
              ],
          }
      },
  }

  file_id = client.upload_file(file=polynomial)["file_id"]

  job_body = client.build_job_body(
      job_type="sample-hamiltonian-integer",
      job_name="dirac-3s-integer-example",
      job_params={
          "device_type": "dirac-3s",
          "num_levels": [3, 4],
          "num_samples": 2,
          "relaxation_schedule": 2,
      },
      polynomial_file_id=file_id,
  )

  assert list(job_body["job_submission"]["device_config"]) == ["dirac-3s"]

  job_id = client.submit_job(job_body=job_body)["job_id"]
  print(f"submitted {job_id}")

  status = JobStatus.SUBMITTED
  while status not in JOB_STATUSES_FINAL:
      status = JobStatus(client.get_job_status(job_id=job_id)["status"])

  response = client.get_job_results(job_id=job_id)

  if status is not JobStatus.COMPLETED:
      print(f"job finished as {status.value}")
      print(response["job_info"])
  else:
      results = response["results"]
      for solution, energy, count in zip(
          results["solutions"], results["energies"], results["counts"]
      ):
          print(f"x = {solution}  H = {energy}  seen {count}x")

.. code-block:: text

  submitted 6534d9d1e4b0a1f2c3d40002
  x = [1, 0]  H = 0.0  seen 1x
  x = [0, 2]  H = 0.0  seen 1x

The assertion above holds because ``dirac-3s`` is never remapped, unlike ``dirac-3``.

Poll against ``JOB_STATUSES_FINAL`` rather than against ``JobStatus.COMPLETED``,
otherwise a failed job loops forever. ``ERRORED`` and ``CANCELLED`` are terminal
too, and leave ``results`` as ``None``, so always check ``status`` before indexing
into ``results``.

This polynomial factors as ``x1^2 * (x1 - 1)^2``, so it reaches zero at ``x1 = 0`` and
``x1 = 1``. Note that ``x2`` does not appear in any term: it is declared by
``num_variables`` and given a level count, but the objective does not constrain it,
so its value varies freely between samples.
