Metadata-Version: 2.4
Name: quapp-merlin
Version: 0.0.1.dev1
Summary: Quapp common library supporting Quapp Platform for Quantum Computing
Author-email: "CITYNOW Co. Ltd. " <corp@citynow.vn>
License: The MIT License (MIT)
        Copyright © CITYNOW Co. Ltd. All rights reserved.
        
        Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Project-URL: Homepage, https://quapp.cloud/
Keywords: quapp,quapp-merlin,quantum,photonic,pytorch
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Requires-Python: <3.15,>=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: quapp-common==0.0.15.dev5
Requires-Dist: merlinquantum==0.4.1
Provides-Extra: dev
Requires-Dist: black; extra == "dev"
Requires-Dist: bumpver; extra == "dev"
Requires-Dist: isort; extra == "dev"
Requires-Dist: pip-tools; extra == "dev"
Requires-Dist: pytest; extra == "dev"
Dynamic: license-file

# quapp-merlin

Quapp provider lib for [MerLin](https://merlinquantum.ai/), Quandela's photonic
quantum machine learning framework. MerLin wraps Perceval but is a separate SDK
here: a MerLin handler returns a **PyTorch model**, not a circuit.

SDK tag: `MERLIN` · providers: `QUAO_QUANTUM_SIMULATOR` (local), `QUANDELA` (cloud)

## What a handler returns

Every other SDK on the platform has `processing()` return a circuit. MerLin has
no circuit to return -- the unit of work is a `torch.nn.Module` holding a
`merlin.QuantumLayer`, plus the batch to run it on.

```python
import merlin as ML
import torch

from quapp_merlin import MerlinJobSpec


def processing(invocation_input: dict) -> MerlinJobSpec:
    layer = ML.QuantumLayer.simple(
            input_size=4,
            measurement_strategy=ML.MeasurementStrategy.probs())

    return MerlinJobSpec(model=layer,
                         inputs=torch.tensor(invocation_input['inputs']),
                         shots=invocation_input.get('shots'))


def post_processing(job_result: dict) -> dict:
    return {'probabilities': job_result['result']['values']}
```

Returning a bare `torch.nn.Module` also works, and the batch is then read from
`invocation_input["inputs"]`.

`shots=None` (the default) means **exact probabilities**: MerLin does not invent
a shot count, and neither does this lib.

## One invocation, one machine

MerLin fans out along three axes, and the first one can put a job on a machine
nobody selected:

| Axis | What it does | Rule here |
|---|---|---|
| Backend | one `MerlinProcessor` binds one backend, but a handler can build a second one and route layers to it | **refused** -- the machine comes from DeviceSelection |
| Chunks | `len(X) > microbatch_size` splits a batch into several cloud jobs | `chunk_concurrency=1`, batch must fit one microbatch |
| Leaves | several `QuantumLayer`s in one model, each its own remote job | one layer per invocation |

Together these keep `future.job_ids` a single id, so `provider_job_id` stays one
value the backend can store as-is. The rules live in
`quapp_merlin/util/single_machine.py` and are locked by
`tests/test_single_machine.py`.

## Local vs remote

| | local (`QUAO_QUANTUM_SIMULATOR`) | remote (`QUANDELA`) |
|---|---|---|
| Runs on | SLOS simulation in the container, **CPU only** | Quandela Cloud, `sim:*` or `qpu:*` |
| Training | yes, `MerlinJobSpec(training=TrainingSpec(...))` | **no** -- no gradient flows through the MerLin processor |
| Phases | COMPILATION, EXECUTION, ANALYSIS | plus POLLING while waiting on the cloud |
| Job id | none | one, from `future.job_ids` |

The image ships the CPU build of torch on purpose: the default wheel drags in
the whole CUDA runtime (~2.5 GB) for a photonic simulator that never touches a
GPU. A job asking for `ProcessingUnit.GPU` runs on CPU with a warning.

## Result shape

`_create_job` returns a dict that is already JSON-safe -- no tensor ever reaches
`json.dumps` in a callback:

```python
{'result': {'values': [[0.24, 0.76]], 'truncated': False, 'elements': 2},
 'shape': [1, 2],
 'batch_size': 1,
 'shots': None,
 'measurement': 'probs',
 'execution_time': 0.184,
 'provider_job_id': None,
 'histogram': {'state_0': 0.24, 'state_1': 0.76},
 'metadata': {}}
```

`histogram` is only produced for a probability measurement of a single row: with
a batch there are as many distributions as rows, and with
`mode_expectations()` / `amplitudes()` there is no distribution over states to
bin. It is `None` in those cases, and the full output stays in `result`.

Large outputs are truncated with `truncated: True` and the real element count,
rather than silently sending the first rows as if they were everything.

## Versions

| | |
|---|---|
| `merlinquantum` | `==0.4.1` -- 0.4 renamed the `QuantumLayer` arguments 0.3 used (`output_mapping_strategy` -> `measurement_strategy`, `ansatz` -> `builder`), so `>=` would break every handler |
| `quapp-common` | `==0.0.15.dev5` -- first release carrying `Sdk.MERLIN` |
| Python | `>=3.10` (merlinquantum), `<3.15` (perceval-quandela) |

`import quapp_merlin` needs none of torch, merlin or perceval: every SDK import
sits inside the function that uses it, so the error classifier still works in an
image where the SDK failed to install. `tests/test_lazy_sdk_imports.py` enforces
it.
