quapp-merlin 0.0.1.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ from .data.merlin_job_spec import MerlinJobSpec, TrainingSpec
2
+ from .factory.merlin_handler_factory import MerlinHandlerFactory
3
+
4
+ # Imported for the side effect: register_rules() has to run before the
5
+ # first job fails, so this import cannot be made lazy.
6
+ from . import error_rules as _error_rules # noqa: E402,F401
7
+
8
+ __all__ = ['MerlinJobSpec', 'TrainingSpec', 'MerlinHandlerFactory']
File without changes
File without changes
@@ -0,0 +1,82 @@
1
+ # Quapp Platform Project
2
+ # merlin_invocation.py
3
+ # Copyright © CITYNOW Co. Ltd. All rights reserved.
4
+
5
+ """Wiring MerLin into the invocation pipeline.
6
+
7
+ The pipeline carries whatever ``processing()`` returned without looking at it --
8
+ except in one place, ``_get_qubit_amount``, which DeviceSelection uses to pick a
9
+ machine. That is the only spot where a MerLin job has to explain itself, and the
10
+ answer is a **mode count**, not a qubit count: see the note on the method.
11
+ """
12
+
13
+ from quapp_common.component.backend.invocation import Invocation
14
+ from quapp_common.data.request.invocation_request import InvocationRequest
15
+ from quapp_common.model.provider.provider import Provider
16
+
17
+ from ...factory.merlin_device_factory import MerlinDeviceFactory
18
+ from ...factory.merlin_provider_factory import MerlinProviderFactory
19
+ from ...util.single_machine import quantum_leaves
20
+
21
+
22
+ class MerlinInvocation(Invocation):
23
+
24
+ def __init__(self, request_data: InvocationRequest):
25
+ super().__init__(request_data)
26
+
27
+ def _export_circuit(self, circuit):
28
+ # Only QISKIT and BRAKET export circuits (EXPORT_CIRCUIT_SDK in
29
+ # quapp-common). A MerLin job is a torch module, not a drawable circuit.
30
+ return None
31
+
32
+ def _create_provider(self):
33
+ self.logger.debug(
34
+ f'Creating provider: '
35
+ f'provider_tag={getattr(self.backend_information, "provider_tag", None)}')
36
+ provider = MerlinProviderFactory.create_provider(
37
+ provider_type=self.backend_information.provider_tag,
38
+ sdk=self.sdk,
39
+ authentication=self.backend_information.authentication)
40
+ self.logger.info(f'Provider created: {type(provider).__name__}')
41
+ return provider
42
+
43
+ def _create_device(self, provider: Provider):
44
+ device_specification = getattr(self.backend_information, 'device_name',
45
+ None)
46
+ self.logger.debug(f'Creating device: {device_specification}')
47
+ device = MerlinDeviceFactory.create_device(
48
+ provider=provider,
49
+ device_specification=device_specification,
50
+ authentication=self.backend_information.authentication,
51
+ sdk=self.sdk)
52
+ self.logger.info(f'Device created: {type(device).__name__}')
53
+ return device
54
+
55
+ def _get_qubit_amount(self, circuit):
56
+ """How many optical **modes** the job needs.
57
+
58
+ DeviceSelection asks for a qubit count because every other SDK is
59
+ gate-based. Photonic hardware is sized in modes, and MerLin's layers
60
+ carry a Perceval circuit whose ``m`` is exactly that. Reporting modes
61
+ here is the closest true answer; a device catalogue that stores qubits
62
+ for a Quandela machine has to be read the same way.
63
+
64
+ Returns 0 rather than raising when the model hides its circuit: failing
65
+ device selection over a metric nobody uses downstream would turn a
66
+ runnable job into a failed one.
67
+ """
68
+ for _, leaf in quantum_leaves(getattr(circuit, 'model', circuit)):
69
+ for attribute in ('m', 'n_modes', 'modes'):
70
+ modes = getattr(leaf, attribute, None)
71
+ if isinstance(modes, int) and modes > 0:
72
+ return modes
73
+
74
+ inner = getattr(leaf, 'circuit', None)
75
+ modes = getattr(inner, 'm', None)
76
+ if isinstance(modes, int) and modes > 0:
77
+ return modes
78
+
79
+ self.logger.warning(
80
+ 'Could not read a mode count from the model; reporting 0 to '
81
+ 'device selection.')
82
+ return 0
File without changes
@@ -0,0 +1,93 @@
1
+ # Quapp Platform Project
2
+ # merlin_job_spec.py
3
+ # Copyright © CITYNOW Co. Ltd. All rights reserved.
4
+
5
+ """What a MerLin handler returns instead of a circuit.
6
+
7
+ Every other SDK in the platform has ``processing()`` return a circuit, and the
8
+ whole pipeline carries that object around opaquely. MerLin has no circuit to
9
+ return: the unit of work is a ``torch.nn.Module`` (normally one holding an
10
+ ``merlin.QuantumLayer``) plus the input tensor to run it on.
11
+
12
+ Returning a bare Module would not be enough -- the model alone says nothing about
13
+ WHAT to run it on, and the device would have to guess which key of
14
+ ``invocation_input`` holds the data. A spec makes the contract explicit and lets
15
+ the device reject a malformed job with a specific error code instead of a
16
+ ``TypeError`` deep inside torch.
17
+
18
+ A bare Module is still accepted as shorthand; see ``coerce()``.
19
+ """
20
+
21
+ from dataclasses import dataclass, field
22
+
23
+
24
+ @dataclass
25
+ class TrainingSpec:
26
+ """Local-only training.
27
+
28
+ MerLin's remote processor cannot backpropagate -- the documentation is
29
+ explicit: *"No gradient can flow through the MerLin processor."* So a spec
30
+ carrying ``training`` is refused on a remote device and only runs on the
31
+ local simulator.
32
+ """
33
+
34
+ targets: object
35
+ epochs: int = 10
36
+ learning_rate: float = 0.01
37
+ loss: str = 'mse'
38
+
39
+
40
+ @dataclass
41
+ class MerlinJobSpec:
42
+ """One MerLin job: one model, one input batch, one machine.
43
+
44
+ @param model: the torch Module to run. Must contain exactly one quantum
45
+ layer -- see the single-machine rules in
46
+ ``model/device/merlin_remote_device.py``.
47
+ @param inputs: the input batch, shape ``[batch, input_size]``. Anything
48
+ ``torch.as_tensor`` accepts is fine (list, ndarray, tensor).
49
+ @param shots: sample count. ``None`` or ``0`` means exact probabilities --
50
+ MerLin does not invent a default, and neither do we.
51
+ @param sampling_method: e.g. ``'multinomial'``; ``None`` leaves MerLin's
52
+ default alone.
53
+ @param training: present only for local training runs.
54
+ @param metadata: free-form, copied into the job result for the handler's own
55
+ post_processing().
56
+ """
57
+
58
+ model: object
59
+ inputs: object
60
+ shots: int = None
61
+ sampling_method: str = None
62
+ training: TrainingSpec = None
63
+ metadata: dict = field(default_factory=dict)
64
+
65
+
66
+ SPEC_HELP = (
67
+ 'processing() must return a MerlinJobSpec, or a torch.nn.Module when the '
68
+ 'input batch is in invocation_input["inputs"].')
69
+
70
+
71
+ def coerce(prepared, invocation_input: dict = None) -> MerlinJobSpec:
72
+ """Turn what ``processing()`` returned into a MerlinJobSpec.
73
+
74
+ A bare Module is accepted because the common case -- one layer, data already
75
+ in the request -- should not need the import. The data then has to be in
76
+ ``invocation_input["inputs"]``; anything else is rejected here rather than
77
+ surfacing later as a shape error inside torch.
78
+ """
79
+ if isinstance(prepared, MerlinJobSpec):
80
+ return prepared
81
+
82
+ if hasattr(prepared, 'forward') and callable(prepared):
83
+ inputs = (invocation_input or {}).get('inputs')
84
+ if inputs is None:
85
+ raise ValueError(
86
+ 'processing() returned a model but no input batch: put it '
87
+ 'in invocation_input["inputs"], or return a MerlinJobSpec '
88
+ 'with inputs=... instead.')
89
+ return MerlinJobSpec(model=prepared, inputs=inputs,
90
+ shots=(invocation_input or {}).get('shots'))
91
+
92
+ raise ValueError(
93
+ f'processing() returned {type(prepared).__name__}. {SPEC_HELP}')
@@ -0,0 +1,190 @@
1
+ # Quapp Platform Project
2
+ # error_rules.py
3
+ # Copyright © CITYNOW Co. Ltd. All rights reserved.
4
+
5
+ """MerLin-specific error classification rules.
6
+
7
+ Registered into ``quapp_common.util.error_classifier`` when ``quapp_merlin`` is
8
+ imported, so ``quapp-common`` never has to depend on MerLin, torch or Perceval.
9
+
10
+ Matching is done on the message text or on the class **NAME as a string**, never
11
+ ``isinstance``, and nothing is imported from ``merlin`` / ``torch`` / ``perceval``:
12
+ the classifier has to work even when the SDKs are absent, and importing torch
13
+ here would make every ``import quapp_merlin`` pay for it.
14
+
15
+ The first group keys off messages **this lib itself** raises. They are the rules
16
+ that matter most, because every single-machine constraint is enforced by raising
17
+ one of them -- without a rule they would all land on a generic
18
+ ``PREPROCESSING_FAILED`` and tell the user nothing about what to change:
19
+
20
+ util/single_machine.py 'Handler attached its own quantum backend'
21
+ 'has N quantum layers'
22
+ 'has no quantum layer'
23
+ 'Input batch has N rows'
24
+ data/merlin_job_spec.py 'processing() returned ...'
25
+ model/device/merlin_remote_device.py
26
+ 'Training cannot run on a remote device'
27
+ 'shots=N exceeds'
28
+ model/provider/quandela_merlin_provider.py
29
+ 'No Quandela Cloud token'
30
+ """
31
+
32
+ from quapp_common.enum.error_category import ErrorCategory
33
+ from quapp_common.util.error_classifier import Rule, register_rules
34
+
35
+ DOC_MACHINE = 'errors/merlin-single-machine'
36
+ DOC_SPEC = 'errors/merlin-job-spec'
37
+ DOC_DEVICE = 'errors/merlin-device'
38
+ DOC_AUTH = 'errors/quandela-auth'
39
+
40
+
41
+ def _message(exception) -> str:
42
+ return str(exception).lower()
43
+
44
+
45
+ def _name(exception) -> str:
46
+ return type(exception).__name__
47
+
48
+
49
+ register_rules(
50
+ # ----------------- raised by quapp-merlin itself (one job, one machine)
51
+ Rule(match=lambda e: 'attached its own quantum backend' in _message(e),
52
+ category=ErrorCategory.USER_CODE,
53
+ code='MERLIN_FOREIGN_BACKEND',
54
+ resolution='The handler built its own quantum backend. A job runs '
55
+ 'on exactly one machine, and the platform chooses it.',
56
+ steps=('Remove the MerlinProcessor / RemoteProcessor / Session '
57
+ 'from the model returned by processing()',
58
+ 'Pick the machine through the device of the job instead'),
59
+ doc_slug=DOC_MACHINE),
60
+
61
+ Rule(match=lambda e: 'quantum layers' in _message(e)
62
+ and 'one invocation runs one' in _message(e),
63
+ category=ErrorCategory.USER_CODE,
64
+ code='MERLIN_MULTI_LAYER_NOT_ALLOWED',
65
+ resolution='The model holds more than one quantum layer; a single '
66
+ 'invocation runs one.',
67
+ steps=('Split the layers across separate invocations',
68
+ 'Or run the whole model on the local simulator'),
69
+ doc_slug=DOC_MACHINE),
70
+
71
+ Rule(match=lambda e: 'no quantum layer' in _message(e),
72
+ category=ErrorCategory.USER_CODE,
73
+ code='MERLIN_NO_QUANTUM_LAYER',
74
+ resolution='The model has no quantum layer, so there is nothing '
75
+ 'for a quantum device to run.',
76
+ steps=('Add a merlin.QuantumLayer to the model',
77
+ 'A purely classical model does not need a quantum job'),
78
+ doc_slug=DOC_SPEC),
79
+
80
+ Rule(match=lambda e: 'input batch has' in _message(e)
81
+ and 'rows' in _message(e),
82
+ category=ErrorCategory.INVALID_INPUT,
83
+ code='MERLIN_BATCH_TOO_LARGE',
84
+ resolution='The input batch is large enough that MerLin would '
85
+ 'split it into several cloud jobs.',
86
+ steps=('Send fewer rows per invocation',
87
+ 'Or run the batch on the local simulator'),
88
+ doc_slug=DOC_MACHINE),
89
+
90
+ Rule(match=lambda e: 'processing() returned' in _message(e)
91
+ or 'must return a merlinjobspec' in _message(e),
92
+ category=ErrorCategory.USER_CODE,
93
+ code='MERLIN_INVALID_JOB_SPEC',
94
+ resolution='processing() did not return something MerLin can run.',
95
+ steps=('Return a MerlinJobSpec(model=..., inputs=...)',
96
+ 'Or return a torch model and put the batch in '
97
+ 'invocation_input["inputs"]'),
98
+ doc_slug=DOC_SPEC),
99
+
100
+ Rule(match=lambda e: 'training cannot run on a remote device' in _message(e),
101
+ category=ErrorCategory.USER_CODE,
102
+ code='MERLIN_TRAINING_NOT_REMOTE',
103
+ resolution='Training cannot run on a remote machine: no gradient '
104
+ 'flows through the MerLin processor.',
105
+ steps=('Train on the local simulator',
106
+ 'Send the trained model to the QPU for inference only'),
107
+ doc_slug=DOC_MACHINE),
108
+
109
+ Rule(match=lambda e: 'exceeds the' in _message(e)
110
+ and 'single cloud call' in _message(e),
111
+ category=ErrorCategory.INVALID_INPUT,
112
+ code='MERLIN_SHOTS_TOO_MANY',
113
+ resolution='The requested shot count is above what one cloud call '
114
+ 'accepts, and would be silently clamped.',
115
+ steps=('Lower shots for this job',
116
+ 'Or split the run across several invocations'),
117
+ doc_slug=DOC_MACHINE),
118
+
119
+ # ------------------------------------------------- Quandela Cloud auth
120
+ Rule(match=lambda e: 'no quandela cloud token' in _message(e),
121
+ category=ErrorCategory.AUTHENTICATION,
122
+ code='QUANDELA_TOKEN_MISSING',
123
+ resolution='No Quandela Cloud token is configured for this '
124
+ 'workspace.',
125
+ steps=('Open Workspace -> Providers -> Quandela',
126
+ 'Add the API token from cloud.quandela.com'),
127
+ doc_slug=DOC_AUTH,
128
+ retryable=False),
129
+
130
+ Rule(match=lambda e: 'unsupported device' in _message(e)
131
+ or 'unsupported provider' in _message(e),
132
+ category=ErrorCategory.PROVIDER_DEVICE,
133
+ code='MERLIN_DEVICE_UNSUPPORTED',
134
+ resolution="This device or provider is not supported by Quapp's "
135
+ 'MerLin lib yet.',
136
+ steps=('Check the device selected for the job',
137
+ 'quapp-merlin serves QUAO_QUANTUM_SIMULATOR and QUANDELA'),
138
+ doc_slug=DOC_DEVICE),
139
+
140
+ # ------------------------------------------------------ torch failures
141
+ # A Fock space grows combinatorially with modes and photons, so the
142
+ # usual way to run out of memory here is an over-ambitious layer, not a
143
+ # platform fault -- the user is the one who can fix it.
144
+ Rule(match=lambda e: _name(e) == 'OutOfMemoryError'
145
+ or 'out of memory' in _message(e)
146
+ or 'cannot allocate' in _message(e),
147
+ category=ErrorCategory.USER_CODE,
148
+ code='MERLIN_OUT_OF_MEMORY',
149
+ resolution='The simulation ran out of memory: the state space of '
150
+ 'this circuit is too large for the function.',
151
+ steps=('Use fewer modes or fewer photons',
152
+ 'Reduce the batch size',
153
+ 'Consider no_bunching or a smaller computation space')),
154
+
155
+ Rule(match=lambda e: _name(e) == 'RuntimeError'
156
+ and ('shapes cannot be multiplied' in _message(e)
157
+ or 'size mismatch' in _message(e)
158
+ or 'must match the size' in _message(e)
159
+ or 'expected input' in _message(e)),
160
+ category=ErrorCategory.USER_CODE,
161
+ code='MERLIN_SHAPE_MISMATCH',
162
+ resolution='The input tensor does not match what the model '
163
+ 'expects.',
164
+ steps=('Check input_size of the quantum layer against the width '
165
+ 'of your input batch',
166
+ 'inputs must have shape [batch, input_size]'),
167
+ doc_slug=DOC_SPEC),
168
+
169
+ # ------------------------------------------- remote execution failures
170
+ Rule(match=lambda e: _name(e) in {'TimeoutError', 'FuturesTimeoutError'}
171
+ or 'timed out' in _message(e),
172
+ category=ErrorCategory.TIMEOUT,
173
+ code='MERLIN_REMOTE_TIMEOUT',
174
+ resolution='The remote machine did not return a result in time.',
175
+ steps=('The QPU queue may be long -- try again later',
176
+ 'Reduce shots or batch size to shorten the run'),
177
+ retryable=True,
178
+ retry_after=300),
179
+
180
+ # Placed last: matching is by NAME, so only these exact names land here.
181
+ Rule(match=lambda e: _name(e) in {'PercevalError', 'ProcessorError',
182
+ 'RPCError', 'CircuitError'},
183
+ category=ErrorCategory.SDK_RUNTIME,
184
+ code='MERLIN_SDK_ERROR',
185
+ resolution='Perceval failed while running the circuit behind '
186
+ 'MerLin. See the stack trace below.',
187
+ steps=('Read the rootCause to see which step was rejected',
188
+ 'Check the mode count and photon count the platform '
189
+ 'supports')),
190
+ )
File without changes
@@ -0,0 +1,42 @@
1
+ # Quapp Platform Project
2
+ # merlin_device_factory.py
3
+ # Copyright © CITYNOW Co. Ltd. All rights reserved.
4
+
5
+ from quapp_common.config.logging_config import logger
6
+ from quapp_common.enum.provider_tag import ProviderTag
7
+ from quapp_common.enum.sdk import Sdk
8
+ from quapp_common.factory.device_factory import DeviceFactory
9
+ from quapp_common.model.provider.provider import Provider
10
+
11
+ from ..model.device.merlin_local_device import MerlinLocalDevice
12
+ from ..model.device.merlin_remote_device import MerlinRemoteDevice
13
+
14
+ logger = logger.bind(context='MerlinDeviceFactory')
15
+
16
+ _MERLIN_SDK = getattr(Sdk, 'MERLIN', None)
17
+
18
+
19
+ class MerlinDeviceFactory(DeviceFactory):
20
+
21
+ @staticmethod
22
+ def create_device(provider: Provider, device_specification: str,
23
+ authentication: dict, sdk: Sdk):
24
+ logger.debug(f'Create Merlin device: {device_specification}')
25
+
26
+ if _MERLIN_SDK is not None and not _MERLIN_SDK.__eq__(sdk):
27
+ raise Exception('Unsupported device!')
28
+
29
+ provider_type = ProviderTag.resolve(
30
+ provider.get_provider_type().value)
31
+
32
+ # One device, one machine: the remote device receives the
33
+ # RemoteProcessor the provider just built and uses nothing else.
34
+ if provider_type is ProviderTag.QUANDELA:
35
+ logger.info('Created remote Merlin device')
36
+ return MerlinRemoteDevice(provider, device_specification)
37
+
38
+ if provider_type is ProviderTag.QUAO_QUANTUM_SIMULATOR:
39
+ logger.info('Created local Merlin device')
40
+ return MerlinLocalDevice(provider, device_specification)
41
+
42
+ raise Exception('Unsupported device!')
@@ -0,0 +1,35 @@
1
+ # Quapp Platform Project
2
+ # merlin_handler_factory.py
3
+ # Copyright © CITYNOW Co. Ltd. All rights reserved.
4
+
5
+ from quapp_common.config.logging_config import job_logger
6
+ from quapp_common.factory.handler_factory import HandlerFactory
7
+ from quapp_common.handler.handler import Handler
8
+
9
+ from ..handler.invocation_handler import InvocationHandler
10
+
11
+
12
+ class MerlinHandlerFactory(HandlerFactory):
13
+
14
+ @staticmethod
15
+ def create_handler(event, circuit_preparation_fn,
16
+ post_processing_fn) -> Handler:
17
+ request_data = event.json()
18
+
19
+ # event.json() returns a DICT, so getattr(..., 'jobId') would always
20
+ # give None and the job_id would never get bound. Read it with .get().
21
+ logger = job_logger(request_data.get('jobId'))
22
+ logger.debug('Create Merlin Handler')
23
+
24
+ provider_job_id = request_data.get('providerJobId')
25
+
26
+ if provider_job_id is None:
27
+ logger.debug('providerJobId is None. Create InvocationHandler')
28
+ return InvocationHandler(
29
+ request_data=request_data,
30
+ circuit_preparation_fn=circuit_preparation_fn,
31
+ post_processing_fn=post_processing_fn)
32
+
33
+ raise ValueError(
34
+ 'MerLin has no job-fetching handler: a run is synchronous and '
35
+ 'its result is returned by the invocation that started it.')
@@ -0,0 +1,36 @@
1
+ # Quapp Platform Project
2
+ # merlin_provider_factory.py
3
+ # Copyright © CITYNOW Co. Ltd. All rights reserved.
4
+
5
+ from quapp_common.config.logging_config import logger
6
+ from quapp_common.enum.provider_tag import ProviderTag
7
+ from quapp_common.enum.sdk import Sdk
8
+ from quapp_common.factory.provider_factory import ProviderFactory
9
+
10
+ from ..model.provider.quandela_merlin_provider import QuandelaMerlinProvider
11
+ from ..model.provider.quapp_merlin_provider import QuappMerlinProvider
12
+
13
+ logger = logger.bind(context='MerlinProviderFactory')
14
+
15
+ _MERLIN_SDK = getattr(Sdk, 'MERLIN', None)
16
+
17
+
18
+ class MerlinProviderFactory(ProviderFactory):
19
+
20
+ @staticmethod
21
+ def create_provider(provider_type: ProviderTag, sdk: Sdk,
22
+ authentication: dict):
23
+ logger.debug(f'Create Merlin provider for {provider_type}')
24
+
25
+ if _MERLIN_SDK is not None and not _MERLIN_SDK.__eq__(sdk):
26
+ raise Exception('Unsupported provider!')
27
+
28
+ if provider_type is ProviderTag.QUANDELA:
29
+ logger.debug('Create Quandela Merlin Provider')
30
+ return QuandelaMerlinProvider(authentication=authentication)
31
+
32
+ if provider_type is ProviderTag.QUAO_QUANTUM_SIMULATOR:
33
+ logger.debug('Create Quapp Merlin Provider')
34
+ return QuappMerlinProvider(authentication=authentication)
35
+
36
+ raise Exception('Unsupported provider!')
File without changes
@@ -0,0 +1,59 @@
1
+ # Quapp Platform Project
2
+ # invocation_handler.py
3
+ # Copyright © CITYNOW Co. Ltd. All rights reserved.
4
+
5
+ """Entry point for a MerLin invocation.
6
+
7
+ One thing differs from the other libs: the handler's ``processing()`` returns a
8
+ model, not a circuit, so it is wrapped here to produce a validated
9
+ ``MerlinJobSpec`` before anything else touches it.
10
+
11
+ The wrapper runs inside ``Invocation.__prepare_circuit``, which means inside
12
+ ``with phase(PREPROCESSING)`` -- so a malformed return value is reported as a
13
+ preprocessing error belonging to the user, which is exactly what it is.
14
+ """
15
+
16
+ from quapp_common.data.request.invocation_request import InvocationRequest
17
+ from quapp_common.handler.handler import Handler
18
+
19
+ from ..component.backend.merlin_invocation import MerlinInvocation
20
+ from ..data.merlin_job_spec import coerce
21
+
22
+
23
+ class InvocationHandler(Handler):
24
+ def __init__(self, request_data: dict, circuit_preparation_fn,
25
+ post_processing_fn):
26
+ super().__init__(request_data, post_processing_fn)
27
+ self.circuit_preparation_fn = circuit_preparation_fn
28
+
29
+ def _prepare_spec(self):
30
+ """``processing()``, with its return value normalised to a job spec."""
31
+
32
+ def prepare(invocation_input):
33
+ prepared = self.circuit_preparation_fn(invocation_input)
34
+ spec = coerce(prepared, invocation_input)
35
+ self.logger.debug(
36
+ f'Job spec ready: model={type(spec.model).__name__}, '
37
+ f'shots={spec.shots}, training={spec.training is not None}')
38
+ return spec
39
+
40
+ return prepare
41
+
42
+ def handle(self):
43
+ self.logger.debug('Handle Invocation: start')
44
+ try:
45
+ invocation_request = InvocationRequest(self.request_data)
46
+ self.logger.debug(f'Invocation request created successfully: '
47
+ f'{invocation_request.__dict__.keys()}')
48
+
49
+ backend = MerlinInvocation(invocation_request)
50
+ self.logger.info('Submitting job via MerlinInvocation')
51
+
52
+ backend.submit_job(
53
+ circuit_preparation_fn=self._prepare_spec(),
54
+ post_processing_fn=self.post_processing_fn)
55
+
56
+ self.logger.debug('Job submission triggered successfully')
57
+ except Exception as exception:
58
+ self.logger.exception(f'Invocation handling failed: {exception}')
59
+ raise
File without changes
File without changes