.. _usage:

=============
Basic Usage
=============

Formatting input polynomial 
----------------------------------

Dirac has the ability to solve higher order polynomials with many variables. In order to illustrate the input format that is used for submitting these problems we will utilize a simple example polynomial:

.. math::
   f(X) = 3 x_4+2.1 x_1^2+1.5 x_2^2+7.9 x_2 x_3+x_2 x_4^2+ x_3^3

The submission input is split into two parts the coefficients and the set of variable indices associated with each coefficient. As an example for above the leading submission coefficients would be created as follows:

.. code-block:: python

   poly_coefs = [3, 2.1, 1.5, 7.9, 1, 1]

The set of variable indices associated with each coefficient will be represented as follows:

.. code-block:: python

   poly_indices = [[0,0,4], [0,1,1], [0,2,2], [0,2,3], [2,4,4], [3,3,3]]

Taking the first polynomial term, :math:`x_4`, which is represented by the first poly index set in the list, :code:`[0,0,4]`, with each 0 indicating a place holder for no variable used for a given degree in the polynomial index set and the 4 in the last place indicating the 4th variable for the input problem.  Additionally, each individual polynomial variable index set has non-decreasing values moving from left to right such that for each set of indices:

.. math::
   [k_1, k_2, k_3, \dots, k_{n-1}, k_n]

must meet following condition:

.. math::
   k_1 \leq k_2 \leq k_3 \leq \dots \leq k_{n-1} \leq k_n

This property guarantees the uniqueness of the submitted polynomial index set. Also, this sparse representation minimizes the amount of data needed to be transferred to the device to obtain a solution. If the same polynomial index set is specified more than once then the coefficients will be added together to create the final polynomial input.


A dual purpose solver
----------------------------------

There are two separate types of of solving routines that can be utilized with Dirac-3 a sum constrained solver and an integer solver.

Integer solver
^^^^^^^^^^^^^^^^

The integer solver will return solutions vector :math:`x_i` based on the associated :code:`num_levels`, :math:`L_i`, such that each solution variable :math:`x_i` will be from the following integer set:

.. math::
   x_i \in \{0, 1, \dots, L_i-1\}

Additionally, the current iteration of the device allows for a maximum of 954 unique optical levels. This implies when considering all :code:`num_levels`, :math:`L_i`, across all variables together they must meet the following constraint in order for the device to accept the input:

.. math::
   \sum_{i=1}^{n} L_i \leq 954

For example if considering a problem where each variable is binary then the maximum number of variables that will be accepted is 477.

A simple example of solving an integer problem based on the input polynomial that was constructed above can be seen below:

.. code-block:: python

    from eqc_direct.client import EqcClient
    
    # MUST FILL IN THE VALUES IN THIS FROM YOUR NETWORKING SETUP FOR THE DEVICE
    eqc_client = EqcClient(ip_address="YOUR DEVICE IP ADDRESS", port="YOUR DEVICE PORT")
    poly_coefs = [3, 2.1, 1.5, 7.9, 1, 1]
    poly_indices = [[0,0,4], [0,1,1], [0,2,2], [0,2,3], [2,4,4], [3,3,3]]
    # use lock to prevent other users from taking exclusive access to the device
    lock_id, start_ts, end_ts=eqc_client.wait_for_lock()
    try:
	result_dict = eqc_client.solve_integer(
	    lock_id=lock_id,
	    poly_indices = poly_indices,
	    poly_coefficients = poly_coefs,
	    relaxation_schedule = 2,
	    num_levels = [2,2,3,4])
	# release lock when finished using the device
    finally:
	lock_release_out = eqc_client.release_lock(lock_id=lock_id)

.. note::
   In order to run the example you must fill in your own network settings to access
   your device.

For more information on additional parameters see :ref:`advanced` and :meth:`eqc_direct.client.EqcClient.solve_integer`.
To learn about the response for this solve routine see :class:`eqc_direct.client.IntegerResult`.

Sum constrained solver
^^^^^^^^^^^^^^^^^^^^^^^^
The sum constrained solver solves for the objective over an affine simplex such that for each solution :math:`X` and sum constraint :math:`R`:

.. math::
   \sum_{i=1}^{n} x_i=R \\
   x_i>0

This implication should be taken into account when analyzing solutions and selecting appropriate problems for this device.

Another important feature for the sum-constrained solver is the ``solution_precision``, which distills the solution so that each :math:`x_i`
is rounded to the unit of precision specified. For example, if ``solution_precision = 0.5`` and ``sum_constraint = 10``, for a solution with values
:math:`[1.25, 5.75, 2.25, 0]`, one possible distilled solution would be :math:`[1, 6, 2.5, 0.5]`. 
Notice that the distilled solution preserves the ``sum_constraint`` while ensuring that each value in the solution is divisible by the ``solution_precision``.

A simple example of solving a sum constrained problem using the :meth:`eqc_direct.client.EqcClient.solve_sum_constrained` can be seen below:


.. code-block:: python

    from eqc_direct.client import EqcClient
    
    # MUST FILL IN THE VALUES IN THIS FROM YOUR NETWORKING SETUP FOR THE DEVICE
    eqc_client = EqcClient(ip_address="YOUR DEVICE IP ADDRESS", port="YOUR DEVICE PORT")
    poly_coefs = [3, 2.1, 1.5, 7.9, 1, 1]
    poly_indices = [[0,0,4], [0,1,1], [0,2,2], [0,2,3], [2,4,4], [3,3,3]]
    # use lock to prevent other users from taking exclusive access to the device
    lock_id, start_ts, end_ts=eqc_client.wait_for_lock()
    try:
	result_dict = eqc_client.solve_sum_constrained(
	    lock_id=lock_id,
	    poly_indices = poly_indices,
	    poly_coefficients = poly_coefs,
	    relaxation_schedule = 2,
	    solution_precision=1,
	    sum_constraint = 100)
    finally:
	# release lock when finished using the device
	lock_release_out = eqc_client.release_lock(lock_id=lock_id)

.. note::
   In order to run the example you must fill in your own network settings to access
   your device.

Each time a job is processed, it blocks for the duration of the device run when using this call. For information regarding results object, see :class:`eqc_direct.client.SumConstrainedResult`.

System monitoring
------------------

:meth:`eqc_direct.client.EqcClient.system_status` returns :code:`status_code` and :code:`status_desc`. For details, see :class:`eqc_direct.utils.SysStatus`. In general, :code:`status_code` greater than or equal to 3 indicates the system is currently in an errored state. See device manual for how to trouble shoot issues based on :code:`status_code`. Displayed below is a simple system status check followed by a print out ofversions associated with device from :meth:`eqc_direct.client.EqcClient.system_info`:

.. code-block:: python

    from eqc_direct.client import EqcClient
    eqc_client = EqcClient(ip_address="YOUR DEVICE IP ADDRESS", port="YOUR DEVICE PORT")
    curr_sys_status = eqc_client.system_status()
    sys_info = eqc_client.system_info()
    print("System status:", curr_sys_status)
    print("System info:", sys_info)
      

Device Lock Basics
--------------------

Device locking ensures that only one process is running on the device at a time. In order to ensure this functionality, there are a few useful functions:

  * :meth:`eqc_direct.client.EqcClient.check_lock`-allows for user to check lock status without attempting acquisition
  * :meth:`eqc_direct.client.EqcClient.acquire_lock`-makes a single attempt to acquire the device lock
  * :meth:`eqc_direct.client.EqcClient.release_lock`-releases lock if lock currently held by the user.

In order to ensure access to users there is a timer on the server that if a user is inactive for more than one minute and another user wishes to use the device they will be able to acquire the device
lock and the previous idle user will lose it's lock on the device. In order to help ease the process of acquiring the lock it's recommended to use
:meth:`eqc_direct.client.EqcClient.wait_for_lock`. In order to ensure maximum device availability it is a good practice to release the lock if taking time between runs on the device. 

    
Health monitoring and calibration for Dirac
----------------------------------------------

Dirac devices periodically automatically calibrate to ensure that it is able to produce high quality results consistently. Similarily, health tests also run on set intervals to ensure that the device is functioning properly. If either of these operations fails the device will enter a hardware fail state see :class:`eqc_direct.utils.SysStatus` for various status codes associated to this state. If the device enters an error state try power cycling the device with switch located on back panel. This will cause the device to calibrate and perform the health testing suite. If hardware failure state persists then contact QCi for further support https://quantumcomputinginc.ladesk.com
