quapp-merlin 0.0.1.dev1__tar.gz → 0.0.1.dev3__tar.gz

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.
Files changed (42) hide show
  1. {quapp_merlin-0.0.1.dev1/quapp_merlin.egg-info → quapp_merlin-0.0.1.dev3}/PKG-INFO +1 -1
  2. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/pyproject.toml +1 -1
  3. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin/model/device/merlin_device.py +34 -0
  4. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin/model/device/merlin_local_device.py +0 -19
  5. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin/model/device/merlin_remote_device.py +10 -18
  6. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3/quapp_merlin.egg-info}/PKG-INFO +1 -1
  7. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin.egg-info/SOURCES.txt +2 -0
  8. quapp_merlin-0.0.1.dev3/tests/test_measurement_kind.py +114 -0
  9. quapp_merlin-0.0.1.dev3/tests/test_remote_eval_mode.py +106 -0
  10. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/LICENSE +0 -0
  11. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/README.md +0 -0
  12. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin/__init__.py +0 -0
  13. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin/component/__init__.py +0 -0
  14. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin/component/backend/__init__.py +0 -0
  15. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin/component/backend/merlin_invocation.py +0 -0
  16. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin/data/__init__.py +0 -0
  17. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin/data/merlin_job_spec.py +0 -0
  18. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin/error_rules.py +0 -0
  19. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin/factory/__init__.py +0 -0
  20. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin/factory/merlin_device_factory.py +0 -0
  21. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin/factory/merlin_handler_factory.py +0 -0
  22. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin/factory/merlin_provider_factory.py +0 -0
  23. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin/handler/__init__.py +0 -0
  24. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin/handler/invocation_handler.py +0 -0
  25. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin/model/__init__.py +0 -0
  26. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin/model/device/__init__.py +0 -0
  27. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin/model/provider/__init__.py +0 -0
  28. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin/model/provider/quandela_merlin_provider.py +0 -0
  29. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin/model/provider/quapp_merlin_provider.py +0 -0
  30. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin/util/__init__.py +0 -0
  31. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin/util/single_machine.py +0 -0
  32. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin/util/tensors.py +0 -0
  33. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin.egg-info/dependency_links.txt +0 -0
  34. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin.egg-info/requires.txt +0 -0
  35. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/quapp_merlin.egg-info/top_level.txt +0 -0
  36. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/setup.cfg +0 -0
  37. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/tests/test_error_logging.py +0 -0
  38. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/tests/test_error_rules.py +0 -0
  39. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/tests/test_job_spec.py +0 -0
  40. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/tests/test_lazy_sdk_imports.py +0 -0
  41. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/tests/test_single_machine.py +0 -0
  42. {quapp_merlin-0.0.1.dev1 → quapp_merlin-0.0.1.dev3}/tests/test_tensors.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quapp-merlin
3
- Version: 0.0.1.dev1
3
+ Version: 0.0.1.dev3
4
4
  Summary: Quapp common library supporting Quapp Platform for Quantum Computing
5
5
  Author-email: "CITYNOW Co. Ltd. " <corp@citynow.vn>
6
6
  License: The MIT License (MIT)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "quapp-merlin"
7
- version = "0.0.1.dev1"
7
+ version = "0.0.1.dev3"
8
8
  description = "Quapp common library supporting Quapp Platform for Quantum Computing"
9
9
  readme = "README.md"
10
10
  authors = [{ name = "CITYNOW Co. Ltd. ", email = "corp@citynow.vn" }]
@@ -110,6 +110,40 @@ class MerlinDevice(CustomDevice, ABC):
110
110
 
111
111
  # ------------------------------------------------------------- helpers
112
112
 
113
+ # A MeasurementStrategy keeps its kind in `.type`, a MeasurementKind enum
114
+ # (PROBABILITIES / MODE_EXPECTATIONS / AMPLITUDES / PARTIAL). It has no
115
+ # `.name` of its own -- the first version of this read
116
+ # type(strategy).__name__ instead, which is always 'MeasurementStrategy',
117
+ # so nothing ever matched 'probs' and no job ever produced a histogram.
118
+ MEASUREMENT_KINDS = {'PROBABILITIES' : 'probs',
119
+ 'MODE_EXPECTATIONS' : 'mode_expectations',
120
+ 'AMPLITUDES' : 'amplitudes',
121
+ 'PARTIAL' : 'partial'}
122
+
123
+ def _measurement_name(self, model) -> str | None:
124
+ """Which measurement the quantum layer uses, as a short string.
125
+
126
+ Only used to decide whether a histogram makes sense, so anything
127
+ unrecognised degrades to None rather than raising.
128
+ """
129
+ modules = model.modules() if hasattr(model, 'modules') else [model]
130
+
131
+ for module in modules:
132
+ strategy = getattr(module, 'measurement_strategy', None)
133
+ if strategy is None:
134
+ continue
135
+
136
+ kind = getattr(strategy, 'type', None)
137
+ name = (getattr(kind, 'name', None) or getattr(kind, 'value', None)
138
+ or kind)
139
+ if name is None:
140
+ return None
141
+
142
+ name = str(name).upper()
143
+ return self.MEASUREMENT_KINDS.get(name, name.lower())
144
+
145
+ return None
146
+
113
147
  def _histogram_from(self, output, shots, batch_size, measurement) -> dict | None:
114
148
  """Bin a single-row probability output into a state -> count histogram.
115
149
 
@@ -174,22 +174,3 @@ class MerlinLocalDevice(MerlinDevice):
174
174
 
175
175
  return output, {'epochs': epochs, 'loss': tensors.plain(history),
176
176
  'final_loss': history[-1] if history else None}
177
-
178
- @staticmethod
179
- def _measurement_name(model) -> str | None:
180
- """Which MeasurementStrategy the quantum layer uses, as a short string.
181
-
182
- Only used to decide whether a histogram makes sense, so an unknown shape
183
- degrades to None instead of raising.
184
- """
185
- for module in (model.modules() if hasattr(model, 'modules') else [model]):
186
- strategy = getattr(module, 'measurement_strategy', None)
187
- if strategy is None:
188
- continue
189
- name = getattr(strategy, 'name', None) or type(strategy).__name__
190
- name = str(name).lower()
191
- for known in ('probs', 'probabilit', 'expectation', 'amplitude'):
192
- if known in name:
193
- return 'probs' if known.startswith('prob') else known
194
- return name
195
- return None
@@ -29,8 +29,7 @@ from quapp_common.model.provider.provider import Provider
29
29
 
30
30
  from .merlin_device import MerlinDevice
31
31
  from ...util import tensors
32
- from ...util.single_machine import (DEFAULT_MAX_BATCH, batch_size_of,
33
- quantum_leaves, reject_foreign_backend,
32
+ from ...util.single_machine import (DEFAULT_MAX_BATCH, reject_foreign_backend,
34
33
  require_batch_fits, require_single_leaf)
35
34
 
36
35
  # How long we are willing to hold the invocation open waiting for the cloud.
@@ -93,9 +92,16 @@ class MerlinRemoteDevice(MerlinDevice):
93
92
  f'Submitting to {self.device_specification} | batch={batch_size} '
94
93
  f'| shots={shots} | timeout={TIMEOUT_SECONDS}s')
95
94
 
95
+ # forward_async refuses a module still in training mode -- "no gradient
96
+ # can flow through the QPU runs" (merlin_processor.py:1109, it reads
97
+ # module.training). A torch Module is training=True from the moment it
98
+ # is built, so a handler that never calls .eval() is rejected every
99
+ # single time. The local device already switched to eval before running
100
+ # inference; this one did not.
101
+ model = spec.model.eval()
102
+
96
103
  started = time.perf_counter()
97
- future = processor.forward_async(spec.model, spec.inputs,
98
- nsample=shots)
104
+ future = processor.forward_async(model, spec.inputs, nsample=shots)
99
105
 
100
106
  job_ids = list(getattr(future, 'job_ids', None) or [])
101
107
  self.logger.info(f'Remote job submitted: job_ids={job_ids}')
@@ -148,17 +154,3 @@ class MerlinRemoteDevice(MerlinDevice):
148
154
  return ','.join(str(job_id) for job_id in job_ids)
149
155
 
150
156
  return str(job_ids[0])
151
-
152
- @staticmethod
153
- def _measurement_name(model) -> str | None:
154
- leaves = quantum_leaves(model)
155
- for _, leaf in leaves:
156
- strategy = getattr(leaf, 'measurement_strategy', None)
157
- if strategy is None:
158
- continue
159
- name = str(getattr(strategy, 'name', None)
160
- or type(strategy).__name__).lower()
161
- if name.startswith('prob') or 'probabilit' in name:
162
- return 'probs'
163
- return name
164
- return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quapp-merlin
3
- Version: 0.0.1.dev1
3
+ Version: 0.0.1.dev3
4
4
  Summary: Quapp common library supporting Quapp Platform for Quantum Computing
5
5
  Author-email: "CITYNOW Co. Ltd. " <corp@citynow.vn>
6
6
  License: The MIT License (MIT)
@@ -34,5 +34,7 @@ tests/test_error_logging.py
34
34
  tests/test_error_rules.py
35
35
  tests/test_job_spec.py
36
36
  tests/test_lazy_sdk_imports.py
37
+ tests/test_measurement_kind.py
38
+ tests/test_remote_eval_mode.py
37
39
  tests/test_single_machine.py
38
40
  tests/test_tensors.py
@@ -0,0 +1,114 @@
1
+ # Quapp Platform Project
2
+ # test_measurement_kind.py
3
+ # Copyright © CITYNOW Co. Ltd. All rights reserved.
4
+
5
+ """Reading the measurement kind off a MerLin strategy.
6
+
7
+ This decides whether a job gets a histogram, and the first version got it
8
+ wrong in a way nothing would have noticed: MeasurementStrategy has no `.name`,
9
+ so `type(strategy).__name__` was read instead -- always the string
10
+ 'MeasurementStrategy' -- nothing ever matched 'probs', and `_histogram_from`
11
+ returned None for every job ever run.
12
+
13
+ The stand-ins mirror merlinquantum 0.4.1 exactly: `MeasurementStrategy.type` is
14
+ a `MeasurementKind` enum whose members are PROBABILITIES, MODE_EXPECTATIONS,
15
+ AMPLITUDES and PARTIAL (merlin/measurement/strategies.py:244).
16
+ """
17
+
18
+ import enum
19
+
20
+ import pytest
21
+
22
+ from quapp_merlin.model.device.merlin_device import MerlinDevice
23
+
24
+
25
+ class MeasurementKind(enum.Enum):
26
+ PROBABILITIES = 'PROBABILITIES'
27
+ MODE_EXPECTATIONS = 'MODE_EXPECTATIONS'
28
+ AMPLITUDES = 'AMPLITUDES'
29
+ PARTIAL = 'PARTIAL'
30
+
31
+
32
+ class MeasurementStrategy:
33
+ """No `.name` -- exactly like the real one. The kind lives in `.type`."""
34
+
35
+ def __init__(self, kind):
36
+ self.type = kind
37
+
38
+
39
+ class Module:
40
+ def __init__(self, children=None, **attributes):
41
+ self._children = children or {}
42
+ self.__dict__.update(attributes)
43
+
44
+ def modules(self):
45
+ yield self
46
+ for child in self._children.values():
47
+ yield from child.modules()
48
+
49
+
50
+ def read(model):
51
+ """_measurement_name does not touch instance state, so call it unbound."""
52
+ return MerlinDevice._measurement_name(MerlinDevice, model)
53
+
54
+
55
+ @pytest.mark.parametrize('kind, expected', [
56
+ (MeasurementKind.PROBABILITIES, 'probs'),
57
+ (MeasurementKind.MODE_EXPECTATIONS, 'mode_expectations'),
58
+ (MeasurementKind.AMPLITUDES, 'amplitudes'),
59
+ (MeasurementKind.PARTIAL, 'partial'),
60
+ ])
61
+ def test_every_kind_is_recognised(kind, expected):
62
+ model = Module(measurement_strategy=MeasurementStrategy(kind))
63
+
64
+ assert read(model) == expected
65
+
66
+
67
+ def test_probabilities_is_what_enables_a_histogram():
68
+ """The whole point: 'probs' is the only value _histogram_from accepts."""
69
+ model = Module(measurement_strategy=MeasurementStrategy(
70
+ MeasurementKind.PROBABILITIES))
71
+
72
+ assert read(model) == 'probs'
73
+
74
+
75
+ def test_the_strategy_is_found_on_a_nested_layer():
76
+ """simple() returns a SimpleSequential wrapper; the strategy sits on the
77
+ QuantumLayer inside it."""
78
+ layer = Module(measurement_strategy=MeasurementStrategy(
79
+ MeasurementKind.PROBABILITIES))
80
+ model = Module(children={'quantum_layer': layer})
81
+
82
+ assert read(model) == 'probs'
83
+
84
+
85
+ def test_class_name_is_never_used_as_the_kind():
86
+ """The regression: reading type(strategy).__name__ gave
87
+ 'measurementstrategy', which matches no kind and silently killed the
88
+ histogram."""
89
+ model = Module(measurement_strategy=MeasurementStrategy(
90
+ MeasurementKind.PROBABILITIES))
91
+
92
+ assert read(model) != 'measurementstrategy'
93
+
94
+
95
+ def test_no_strategy_at_all():
96
+ assert read(Module()) is None
97
+
98
+
99
+ def test_unknown_kind_degrades_instead_of_raising():
100
+ class Odd(enum.Enum):
101
+ SOMETHING_NEW = 'SOMETHING_NEW'
102
+
103
+ model = Module(measurement_strategy=MeasurementStrategy(Odd.SOMETHING_NEW))
104
+
105
+ assert read(model) == 'something_new'
106
+
107
+
108
+ def test_a_bare_module_without_modules_still_works():
109
+ """A handler may return the layer itself rather than a wrapper."""
110
+
111
+ class Bare:
112
+ measurement_strategy = MeasurementStrategy(MeasurementKind.AMPLITUDES)
113
+
114
+ assert read(Bare()) == 'amplitudes'
@@ -0,0 +1,106 @@
1
+ # Quapp Platform Project
2
+ # test_remote_eval_mode.py
3
+ # Copyright © CITYNOW Co. Ltd. All rights reserved.
4
+
5
+ """A model sent to the cloud has to be in eval mode.
6
+
7
+ MerlinProcessor refuses anything else::
8
+
9
+ if module.training:
10
+ raise RuntimeError(
11
+ "Backend quantum execution requires `.eval()` mode because no
12
+ gradient can flow through the QPU runs...")
13
+
14
+ -- merlinquantum 0.4.1, merlin/core/merlin_processor.py:1109.
15
+
16
+ A torch Module is ``training=True`` from the moment it is constructed, so a
17
+ handler that never calls ``.eval()`` -- which is every handler, because nothing
18
+ in the contract tells them to -- was rejected on every single remote job. The
19
+ local device already switched to eval before inference; the remote one did not.
20
+
21
+ Checked by reading the source rather than by running a model: importing merlin
22
+ would drag torch into the test suite, and the rule being enforced is "the call
23
+ is there, before the submit", which the AST answers exactly.
24
+ """
25
+
26
+ import ast
27
+ import pathlib
28
+
29
+ import pytest
30
+
31
+ REMOTE_DEVICE = (pathlib.Path(__file__).resolve().parent.parent
32
+ / 'quapp_merlin' / 'model' / 'device'
33
+ / 'merlin_remote_device.py')
34
+ LOCAL_DEVICE = (pathlib.Path(__file__).resolve().parent.parent
35
+ / 'quapp_merlin' / 'model' / 'device'
36
+ / 'merlin_local_device.py')
37
+
38
+
39
+ def create_job(path):
40
+ tree = ast.parse(path.read_text(encoding='utf-8'), filename=str(path))
41
+ for node in ast.walk(tree):
42
+ if isinstance(node, ast.FunctionDef) and node.name == '_create_job':
43
+ return node
44
+ raise AssertionError(f'_create_job() not found in {path.name}')
45
+
46
+
47
+ def call_lines(node, attribute) -> list:
48
+ """Line numbers of every ``x.<attribute>(...)`` call inside a function."""
49
+ return sorted(inner.lineno for inner in ast.walk(node)
50
+ if isinstance(inner, ast.Call)
51
+ and getattr(inner.func, 'attr', None) == attribute)
52
+
53
+
54
+ def test_remote_switches_the_model_to_eval():
55
+ assert call_lines(create_job(REMOTE_DEVICE), 'eval'), (
56
+ 'merlin_remote_device._create_job never calls .eval(); MerlinProcessor '
57
+ 'rejects a module in training mode and every remote job fails')
58
+
59
+
60
+ def test_eval_happens_before_the_submit():
61
+ """Calling it after forward_async would be too late to matter."""
62
+ node = create_job(REMOTE_DEVICE)
63
+
64
+ eval_at = call_lines(node, 'eval')
65
+ submit_at = call_lines(node, 'forward_async')
66
+
67
+ assert submit_at, 'no forward_async call in _create_job'
68
+ assert min(eval_at) < min(submit_at), (
69
+ f'.eval() is at line {min(eval_at)} but forward_async is at '
70
+ f'{min(submit_at)} -- the model is still in training mode when it is '
71
+ f'submitted')
72
+
73
+
74
+ def test_the_model_that_is_submitted_is_the_one_that_was_evalled():
75
+ """`spec.model.eval()` returns the module, so the submit has to use that
76
+ return value -- passing spec.model again would work only by side effect and
77
+ would break the moment eval() stops returning self."""
78
+ node = create_job(REMOTE_DEVICE)
79
+
80
+ submitted = None
81
+ for inner in ast.walk(node):
82
+ if isinstance(inner, ast.Call) and \
83
+ getattr(inner.func, 'attr', None) == 'forward_async':
84
+ submitted = inner.args[0] if inner.args else None
85
+
86
+ assert isinstance(submitted, ast.Name), (
87
+ 'forward_async should be handed the local name bound from '
88
+ 'spec.model.eval(), not an attribute read straight off the spec')
89
+
90
+
91
+ def test_local_device_also_evals_before_inference():
92
+ """Same rule on the simulator path, where it was already right -- a
93
+ regression there would be just as invisible."""
94
+ assert call_lines(create_job(LOCAL_DEVICE), 'eval')
95
+
96
+
97
+ @pytest.mark.parametrize('path', [REMOTE_DEVICE, LOCAL_DEVICE],
98
+ ids=lambda p: p.name)
99
+ def test_no_train_mode_on_the_inference_path(path):
100
+ """`.train()` belongs to the training loop only. On a device that submits to
101
+ a QPU it would put the module straight back into the state MerLin refuses."""
102
+ node = create_job(path)
103
+
104
+ assert not call_lines(node, 'train'), (
105
+ f'{path.name}: _create_job calls .train(); the training loop is the '
106
+ f'only place that may do that')