quantum-mind 2.0.0__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.
Files changed (93) hide show
  1. quantum_mind/__init__.py +25 -0
  2. quantum_mind/_optional.py +14 -0
  3. quantum_mind/applications/README.md +19 -0
  4. quantum_mind/applications/__init__.py +1 -0
  5. quantum_mind/applications/calibration.py +477 -0
  6. quantum_mind/applications/fusion.py +259 -0
  7. quantum_mind/applications/handover.py +197 -0
  8. quantum_mind/applications/intent.py +196 -0
  9. quantum_mind/applications/orchestration.py +381 -0
  10. quantum_mind/applications/personalisation.py +242 -0
  11. quantum_mind/applications/questioning.py +339 -0
  12. quantum_mind/applications/robotics.py +428 -0
  13. quantum_mind/circuits/README.md +50 -0
  14. quantum_mind/circuits/__init__.py +5 -0
  15. quantum_mind/circuits/backends.py +198 -0
  16. quantum_mind/circuits/builders.py +414 -0
  17. quantum_mind/core/__init__.py +4 -0
  18. quantum_mind/core/fit.py +370 -0
  19. quantum_mind/core/linalg.py +341 -0
  20. quantum_mind/core/model.py +283 -0
  21. quantum_mind/data/__init__.py +58 -0
  22. quantum_mind/envs/__init__.py +14 -0
  23. quantum_mind/envs/hri.py +329 -0
  24. quantum_mind/families/__init__.py +8 -0
  25. quantum_mind/families/concepts/README.md +20 -0
  26. quantum_mind/families/concepts/__init__.py +3 -0
  27. quantum_mind/families/concepts/models.py +148 -0
  28. quantum_mind/families/conjunction/README.md +58 -0
  29. quantum_mind/families/conjunction/__init__.py +3 -0
  30. quantum_mind/families/conjunction/models.py +182 -0
  31. quantum_mind/families/contextuality/README.md +30 -0
  32. quantum_mind/families/contextuality/__init__.py +2 -0
  33. quantum_mind/families/contextuality/models.py +129 -0
  34. quantum_mind/families/decision/README.md +42 -0
  35. quantum_mind/families/decision/__init__.py +2 -0
  36. quantum_mind/families/decision/models.py +171 -0
  37. quantum_mind/families/dynamics/README.md +51 -0
  38. quantum_mind/families/dynamics/__init__.py +3 -0
  39. quantum_mind/families/dynamics/models.py +474 -0
  40. quantum_mind/families/game_theory/README.md +24 -0
  41. quantum_mind/families/game_theory/__init__.py +2 -0
  42. quantum_mind/families/game_theory/models.py +225 -0
  43. quantum_mind/families/interference/README.md +59 -0
  44. quantum_mind/families/interference/__init__.py +3 -0
  45. quantum_mind/families/interference/models.py +129 -0
  46. quantum_mind/families/memory/README.md +18 -0
  47. quantum_mind/families/memory/__init__.py +2 -0
  48. quantum_mind/families/memory/models.py +132 -0
  49. quantum_mind/families/order_effects/README.md +73 -0
  50. quantum_mind/families/order_effects/__init__.py +3 -0
  51. quantum_mind/families/order_effects/models.py +362 -0
  52. quantum_mind/families/perception/README.md +23 -0
  53. quantum_mind/families/perception/__init__.py +3 -0
  54. quantum_mind/families/perception/models.py +220 -0
  55. quantum_mind/families/qlbn/README.md +56 -0
  56. quantum_mind/families/qlbn/__init__.py +3 -0
  57. quantum_mind/families/qlbn/models.py +273 -0
  58. quantum_mind/families/similarity/README.md +35 -0
  59. quantum_mind/families/similarity/__init__.py +2 -0
  60. quantum_mind/families/similarity/models.py +200 -0
  61. quantum_mind/inspired/README.md +38 -0
  62. quantum_mind/inspired/__init__.py +27 -0
  63. quantum_mind/inspired/exploration.py +347 -0
  64. quantum_mind/inspired/optimisers.py +300 -0
  65. quantum_mind/inspired/rl.py +306 -0
  66. quantum_mind/inspired/tensor.py +299 -0
  67. quantum_mind/inspired/tensor_layers.py +270 -0
  68. quantum_mind/inspired/text.py +233 -0
  69. quantum_mind/problems/README.md +20 -0
  70. quantum_mind/problems/__init__.py +288 -0
  71. quantum_mind/quantum/README.md +62 -0
  72. quantum_mind/quantum/__init__.py +41 -0
  73. quantum_mind/quantum/algorithms.py +493 -0
  74. quantum_mind/quantum/ansatz.py +208 -0
  75. quantum_mind/quantum/classifiers.py +692 -0
  76. quantum_mind/quantum/estimation.py +224 -0
  77. quantum_mind/quantum/fourier.py +145 -0
  78. quantum_mind/quantum/policy.py +210 -0
  79. quantum_mind/quantum/quanvolution.py +173 -0
  80. quantum_mind/quantum/statevector.py +566 -0
  81. quantum_mind/quantum/walks.py +170 -0
  82. quantum_mind/viz/README.md +44 -0
  83. quantum_mind/viz/__init__.py +32 -0
  84. quantum_mind/viz/interactive.py +334 -0
  85. quantum_mind/viz/mpl.py +350 -0
  86. quantum_mind/viz/states.py +447 -0
  87. quantum_mind/viz/themes.py +30 -0
  88. quantum_mind-2.0.0.dist-info/METADATA +600 -0
  89. quantum_mind-2.0.0.dist-info/RECORD +93 -0
  90. quantum_mind-2.0.0.dist-info/WHEEL +5 -0
  91. quantum_mind-2.0.0.dist-info/licenses/LICENSE +201 -0
  92. quantum_mind-2.0.0.dist-info/licenses/NOTICE +5 -0
  93. quantum_mind-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,259 @@
1
+ r"""Multimodal fusion for robots: from detector scores, speech and gaze to one belief over intents.
2
+
3
+ A request such as "bring me that cup" is resolved from several sources: an object detector's scores,
4
+ the speech recogniser's n-best list, where the person looks and where they point. This module turns
5
+ each source into a *likelihood over the same hypotheses* and offers three ways to combine them:
6
+
7
+ * :func:`bayes_fusion`: independent cues multiply (order-free);
8
+ * :func:`dempster_shafer_fusion`: Dempster's rule with discounted sources, which keeps an explicit
9
+ "don't know" mass when sources conflict;
10
+ * :class:`~quantum_mind.applications.intent.QuantumIntentResolver`: quantum-like fusion in which incompatible
11
+ cues are applied as rotated Kraus operators, so cue order matters (it reduces to Bayes when every
12
+ incompatibility angle is zero); :func:`fit_incompatibility` estimates the angles from logged
13
+ choices, and :func:`compare_fusion` compares the three rules on held-out data.
14
+
15
+ Adapters: :func:`detector_likelihood`, :func:`asr_likelihood`, :func:`direction_likelihood` (gaze or
16
+ pointing).
17
+
18
+ Examples
19
+ --------
20
+ >>> import numpy as np
21
+ >>> from quantum_mind.applications.fusion import detector_likelihood, direction_likelihood, bayes_fusion
22
+ >>> objects = ['red cup', 'blue cup', 'bowl']
23
+ >>> vision = detector_likelihood({'red cup': 0.6, 'blue cup': 0.5, 'bowl': 0.2}, objects)
24
+ >>> gaze = direction_likelihood([1, 0, 0], {'red cup': [0.9, 0.1, 0], 'blue cup': [0, 1, 0],
25
+ ... 'bowl': [-1, 0, 0]}, objects, kappa=4)
26
+ >>> post = bayes_fusion([vision, gaze])
27
+ >>> objects[int(np.argmax(post))]
28
+ 'red cup'
29
+ """
30
+ from __future__ import annotations
31
+
32
+ import numpy as np
33
+ from scipy.optimize import minimize
34
+
35
+ __all__ = ['detector_likelihood', 'asr_likelihood', 'direction_likelihood', 'bayes_fusion',
36
+ 'dempster_shafer_fusion', 'fit_incompatibility', 'compare_fusion']
37
+
38
+
39
+ def _norm(v, floor):
40
+ """Floor and normalise a non-negative vector."""
41
+ v = np.maximum(np.asarray(v, float), 0) + floor
42
+ return v / v.sum()
43
+
44
+
45
+ def detector_likelihood(scores, hypotheses, temperature=1.0, floor=1e-3):
46
+ """Likelihood over hypotheses from an object detector's scores.
47
+
48
+ Parameters
49
+ ----------
50
+ scores : dict or array_like
51
+ ``{hypothesis: score}`` (missing hypotheses score 0) or scores in the order of ``hypotheses``.
52
+ Scores are confidences in [0, 1] or logits.
53
+ hypotheses : sequence
54
+ Hypotheses (object or intent names).
55
+ temperature : float, optional
56
+ Softens (> 1) or sharpens (< 1) the scores before normalising.
57
+ floor : float, optional
58
+ Added to every entry so that no hypothesis is ruled out by one cue.
59
+
60
+ Returns
61
+ -------
62
+ numpy.ndarray
63
+ Normalised likelihood, one entry per hypothesis.
64
+ """
65
+ s = np.array([scores.get(h, 0.0) for h in hypotheses], float) if isinstance(scores, dict) else np.asarray(scores, float)
66
+ if np.all((s >= 0) & (s <= 1)): # confidences: power-sharpen or soften
67
+ return _norm(s ** (1.0 / temperature), floor)
68
+ z = s / temperature # logits: softmax
69
+ return _norm(np.exp(z - z.max()), floor)
70
+
71
+
72
+ def asr_likelihood(nbest, keywords, hypotheses, floor=0.02):
73
+ """Likelihood over hypotheses from a speech recogniser's n-best list.
74
+
75
+ Parameters
76
+ ----------
77
+ nbest : sequence of tuple
78
+ ``[(transcript, confidence), ...]``.
79
+ keywords : dict
80
+ ``{hypothesis: [words that refer to it]}``.
81
+ hypotheses : sequence
82
+ floor : float, optional
83
+
84
+ Returns
85
+ -------
86
+ numpy.ndarray
87
+ Each transcript's confidence is shared among the hypotheses it mentions (or spread over all
88
+ hypotheses when it mentions none).
89
+ """
90
+ L = np.zeros(len(hypotheses))
91
+ for text, conf in nbest:
92
+ words = set(text.lower().replace(',', ' ').split())
93
+ hit = np.array([any(k.lower() in words or k.lower() in text.lower() for k in keywords.get(h, [])) for h in hypotheses])
94
+ L += conf * (hit / hit.sum() if hit.any() else np.full(len(hypotheses), 1 / len(hypotheses)))
95
+ return _norm(L, floor)
96
+
97
+
98
+ def direction_likelihood(direction, targets, hypotheses, kappa=5.0, floor=1e-3):
99
+ """Likelihood over hypotheses from a gaze or pointing direction (von Mises-Fisher).
100
+
101
+ Parameters
102
+ ----------
103
+ direction : array_like
104
+ Direction vector (2D or 3D) from the person's head or hand.
105
+ targets : dict
106
+ ``{hypothesis: direction to that object}`` from the same origin.
107
+ hypotheses : sequence
108
+ kappa : float, optional
109
+ Concentration: larger values trust the direction more.
110
+ floor : float, optional
111
+
112
+ Returns
113
+ -------
114
+ numpy.ndarray
115
+ :math:`\\propto \\exp(\\kappa \\cos \\angle(d, t_h))`.
116
+ """
117
+ d = np.asarray(direction, float)
118
+ d = d / np.linalg.norm(d)
119
+ cos = []
120
+ for h in hypotheses:
121
+ t = np.asarray(targets[h], float)
122
+ cos.append(float(d @ t / np.linalg.norm(t)))
123
+ return _norm(np.exp(kappa * (np.array(cos) - 1)), floor)
124
+
125
+
126
+ def bayes_fusion(likelihoods, prior=None):
127
+ """Posterior of independent cues: prior times the product of the likelihoods.
128
+
129
+ Parameters
130
+ ----------
131
+ likelihoods : sequence of array_like
132
+ One likelihood vector per cue.
133
+ prior : array_like, optional
134
+ Prior over hypotheses (uniform by default).
135
+
136
+ Returns
137
+ -------
138
+ numpy.ndarray
139
+ """
140
+ L = np.asarray(likelihoods, float)
141
+ post = (np.full(L.shape[1], 1 / L.shape[1]) if prior is None else np.asarray(prior, float)) * np.prod(L, axis=0)
142
+ return post / post.sum()
143
+
144
+
145
+ def dempster_shafer_fusion(likelihoods, reliabilities=None):
146
+ """Dempster's rule for singleton masses with discounting.
147
+
148
+ Each cue becomes a mass function on the single hypotheses, discounted by its reliability r: a
149
+ fraction 1 - r of its mass goes to "any hypothesis" (ignorance). Conflict between cues is
150
+ renormalised away by Dempster's rule.
151
+
152
+ Parameters
153
+ ----------
154
+ likelihoods : sequence of array_like
155
+ One normalised likelihood per cue.
156
+ reliabilities : sequence of float, optional
157
+ Reliability of each cue in [0, 1] (default 0.9).
158
+
159
+ Returns
160
+ -------
161
+ belief : numpy.ndarray
162
+ Pignistic probability over hypotheses (ignorance shared equally).
163
+ ignorance : float
164
+ Remaining mass on "any hypothesis".
165
+ """
166
+ L = np.asarray(likelihoods, float)
167
+ K = L.shape[1]
168
+ r = np.full(len(L), 0.9) if reliabilities is None else np.asarray(reliabilities, float)
169
+ m = np.zeros(K)
170
+ omega = 1.0 # mass on the whole frame (start: total ignorance)
171
+ for li, ri in zip(L, r):
172
+ mi = ri * li / li.sum()
173
+ wi = 1 - ri
174
+ # combine (m, omega) with (mi, wi) on singletons plus the frame
175
+ new = m * mi + m * wi + omega * mi
176
+ new_omega = omega * wi
177
+ z = new.sum() + new_omega # 1 - conflict
178
+ m, omega = new / z, new_omega / z
179
+ return m + omega / K, float(omega)
180
+
181
+
182
+ def fit_incompatibility(resolver_cls, intents, cue_likelihoods, trials, prior=None, bounds=(0.0, np.pi / 2)):
183
+ """Fit the incompatibility angle of each cue of a quantum-like resolver from logged choices.
184
+
185
+ Parameters
186
+ ----------
187
+ resolver_cls : type
188
+ :class:`~quantum_mind.applications.intent.QuantumIntentResolver`.
189
+ intents : sequence of str
190
+ Intent names.
191
+ cue_likelihoods : dict
192
+ ``{cue name: likelihood over intents}``.
193
+ trials : sequence of tuple
194
+ ``[(cue order, chosen intent), ...]``: the order in which cues arrived and what the person
195
+ actually wanted (or chose).
196
+ prior : array_like, optional
197
+ Prior over intents.
198
+ bounds : tuple, optional
199
+ Range of the angles.
200
+
201
+ Returns
202
+ -------
203
+ resolver : QuantumIntentResolver
204
+ Resolver with the fitted angles.
205
+ angles : dict
206
+ ``{cue name: fitted angle}``.
207
+ """
208
+ names = list(cue_likelihoods)
209
+ idx = {h: i for i, h in enumerate(intents)}
210
+
211
+ def build(theta):
212
+ r = resolver_cls(intents, prior)
213
+ for n, t in zip(names, theta):
214
+ r.add_cue(n, cue_likelihoods[n], t)
215
+ return r
216
+
217
+ def nll(theta):
218
+ r = build(theta)
219
+ return -sum(np.log(max(r.posterior(order)[idx[c]], 1e-12)) for order, c in trials)
220
+
221
+ best = min((minimize(nll, np.full(len(names), x0), method='L-BFGS-B', bounds=[bounds] * len(names))
222
+ for x0 in (0.05, 0.4, 0.9)), key=lambda r: r.fun)
223
+ return build(best.x), dict(zip(names, (float(v) for v in best.x)))
224
+
225
+
226
+ def compare_fusion(intents, cue_likelihoods, train, test, prior=None):
227
+ """Compare Bayesian, Dempster-Shafer and fitted quantum-like fusion on held-out trials.
228
+
229
+ Parameters
230
+ ----------
231
+ intents : sequence of str
232
+ cue_likelihoods : dict
233
+ ``{cue name: likelihood over intents}``.
234
+ train, test : sequence of tuple
235
+ ``[(cue order, chosen intent), ...]``; the quantum-like angles are fitted on ``train``.
236
+ prior : array_like, optional
237
+
238
+ Returns
239
+ -------
240
+ dict
241
+ ``{rule: {'log_loss': ..., 'accuracy': ...}}`` on ``test`` (lower log loss is better), plus
242
+ ``'angles'``.
243
+ """
244
+ from .intent import QuantumIntentResolver
245
+ idx = {h: i for i, h in enumerate(intents)}
246
+ q, angles = fit_incompatibility(QuantumIntentResolver, intents, cue_likelihoods, train, prior)
247
+ rules = {
248
+ 'bayes': lambda order: bayes_fusion([cue_likelihoods[c] for c in order], prior),
249
+ 'dempster_shafer': lambda order: dempster_shafer_fusion([cue_likelihoods[c] for c in order])[0],
250
+ 'quantum_like': q.posterior,
251
+ }
252
+ out = {}
253
+ for name, rule in rules.items():
254
+ P = [rule(order) for order, _ in test]
255
+ y = [idx[c] for _, c in test]
256
+ out[name] = {'log_loss': float(-np.mean([np.log(max(p[i], 1e-12)) for p, i in zip(P, y)])),
257
+ 'accuracy': float(np.mean([int(np.argmax(p)) == i for p, i in zip(P, y)]))}
258
+ out['angles'] = angles
259
+ return out
@@ -0,0 +1,197 @@
1
+ r"""Trust-aware hand-over: a robot policy built on the trust qubit.
2
+
3
+ The robot hands objects to a person. Whether the person takes the object smoothly depends on how much
4
+ they trust the robot at that moment, and trust changes with every success and failure
5
+ (:class:`~quantum_mind.families.dynamics.OpenSystemBelief`). The robot can also *ask* about trust, which tells it
6
+ where trust stands but, in the quantum-like model, also changes it (the question collapses the belief).
7
+
8
+ At each step the policy compares the expected cost of four actions:
9
+
10
+ ================= ==========================================================================
11
+ ``handover`` normal speed; fails with probability :math:`1 - p_{\text{trust}}`
12
+ ``slow_handover`` slower and more legible; costs ``slow_cost`` but halves the failure risk
13
+ ``ask`` costs ``ask_cost``; the answer collapses the belief, then the better of the
14
+ two hand-overs is chosen for the next step (one-step look-ahead)
15
+ ``wait`` costs ``wait_cost``; the belief keeps dephasing toward its mixed state
16
+ ================= ==========================================================================
17
+
18
+ The same policy can run on the classical :class:`~quantum_mind.families.dynamics.MarkovBelief`, where asking
19
+ only reads trust; comparing the two shows when the order-aware model changes the robot's behaviour.
20
+
21
+ Examples
22
+ --------
23
+ >>> from quantum_mind.families.dynamics import OpenSystemBelief
24
+ >>> from quantum_mind.applications.handover import TrustAwareHandover
25
+ >>> policy = TrustAwareHandover(OpenSystemBelief(phi0=0.6, a_pos=0.8, a_neg=1.2, gamma=0.3))
26
+ >>> policy.decide()['action'] # high trust: hand over normally
27
+ 'handover'
28
+ >>> policy.observe(0); policy.observe(0) # two failures
29
+ >>> policy.decide()['action'] != 'handover'
30
+ True
31
+ """
32
+ from __future__ import annotations
33
+
34
+ import numpy as np
35
+
36
+ __all__ = ['TrustAwareHandover', 'simulate_handover_session']
37
+
38
+
39
+ class TrustAwareHandover:
40
+ """Hand-over policy on a trust belief.
41
+
42
+ Parameters
43
+ ----------
44
+ model : OpenSystemBelief or MarkovBelief
45
+ Trust model, with its parameters (fitted to the person or the population).
46
+ fail_cost : float, optional
47
+ Cost of a failed hand-over (dropped object, startled person).
48
+ slow_cost : float, optional
49
+ Extra cost of a slow hand-over.
50
+ ask_cost : float, optional
51
+ Cost of asking "do you trust me to hand this over?".
52
+ wait_cost : float, optional
53
+ Cost of waiting one step.
54
+ slow_factor : float, optional
55
+ Failure probability of a slow hand-over relative to a normal one.
56
+
57
+ Attributes
58
+ ----------
59
+ rho : numpy.ndarray
60
+ Current trust belief as a 2 x 2 density matrix (open-system model), or ``None``.
61
+ p : float
62
+ Current P(trust) (Markov model), or ``None``.
63
+ """
64
+
65
+ def __init__(self, model, fail_cost=10.0, slow_cost=1.0, ask_cost=0.5, wait_cost=0.3, slow_factor=0.5):
66
+ self.model = model
67
+ self.fail_cost, self.slow_cost, self.ask_cost = fail_cost, slow_cost, ask_cost
68
+ self.wait_cost, self.slow_factor = wait_cost, slow_factor
69
+ self.quantum = hasattr(model, 'phi0') and hasattr(model, 'gamma')
70
+ if self.quantum:
71
+ v = np.array([np.cos(model.phi0 / 2), np.sin(model.phi0 / 2)])
72
+ self.rho = np.outer(v, v)
73
+ self.p = None
74
+ else:
75
+ self.rho = None
76
+ self.p = float(model.p0)
77
+
78
+ # ------------------------------------------------------------------ belief
79
+ @property
80
+ def p_trust(self):
81
+ """float: current probability that the person trusts the robot (says "yes")."""
82
+ return float(np.clip(self.rho[0, 0], 0, 1)) if self.quantum else self.p
83
+
84
+ def _rotate(self, rho, event):
85
+ """Open-system update after one event: rotation, then dephasing."""
86
+ a = -self.model.a_pos if event else self.model.a_neg
87
+ c, s = np.cos(a / 2), np.sin(a / 2)
88
+ U = np.array([[c, -s], [s, c]])
89
+ r = U @ rho @ U.T
90
+ f = 1 - self.model.gamma
91
+ return np.array([[r[0, 0], f * r[0, 1]], [f * r[1, 0], r[1, 1]]])
92
+
93
+ def observe(self, event):
94
+ """Update the belief after a hand-over outcome.
95
+
96
+ Parameters
97
+ ----------
98
+ event : int
99
+ 1 for a success, 0 for a failure.
100
+ """
101
+ if self.quantum:
102
+ self.rho = self._rotate(self.rho, event)
103
+ else:
104
+ self.p = self.model.step(self.p, event)
105
+
106
+ def answer(self, yes):
107
+ """Update the belief after the person answers the trust question.
108
+
109
+ In the open-system model the answer collapses the belief onto "trust" or "no trust"; in the
110
+ Markov model it sets P(trust) to 1 or 0.
111
+
112
+ Parameters
113
+ ----------
114
+ yes : bool or int
115
+ """
116
+ if self.quantum:
117
+ self.rho = np.diag([1.0, 0.0]) if yes else np.diag([0.0, 1.0])
118
+ else:
119
+ self.p = 1.0 if yes else 0.0
120
+
121
+ def wait(self):
122
+ """Let one step pass without a hand-over (dephasing only, open-system model)."""
123
+ if self.quantum:
124
+ f = 1 - self.model.gamma
125
+ self.rho = np.array([[self.rho[0, 0], f * self.rho[0, 1]], [f * self.rho[1, 0], self.rho[1, 1]]])
126
+
127
+ # ------------------------------------------------------------------ decision
128
+ def _handover_costs(self, p):
129
+ """Expected cost of a normal and a slow hand-over at trust probability p."""
130
+ return self.fail_cost * (1 - p), self.slow_cost + self.fail_cost * (1 - p) * self.slow_factor
131
+
132
+ def decide(self):
133
+ """Expected cost of each action and the best one.
134
+
135
+ Returns
136
+ -------
137
+ dict
138
+ ``action`` (the cheapest), ``costs`` (``{action: expected cost}``) and ``p_trust``.
139
+ """
140
+ p = self.p_trust
141
+ normal, slow = self._handover_costs(p)
142
+ # asking: pay ask_cost, then hand over optimally given the collapsed answer
143
+ after_yes, after_no = min(self._handover_costs(1.0)), min(self._handover_costs(0.0))
144
+ ask = self.ask_cost + p * after_yes + (1 - p) * after_no
145
+ costs = {'handover': normal, 'slow_handover': slow, 'ask': ask, 'wait': self.wait_cost + min(normal, slow)}
146
+ return {'action': min(costs, key=costs.get), 'costs': costs, 'p_trust': p}
147
+
148
+
149
+ def simulate_handover_session(policy, person, events_rng, n_steps=20):
150
+ """Run a policy against a simulated person whose trust follows a model.
151
+
152
+ The person's real trust (a separate copy of the model) determines whether each hand-over succeeds
153
+ and how the trust question is answered; the policy only sees outcomes and answers.
154
+
155
+ Parameters
156
+ ----------
157
+ policy : TrustAwareHandover
158
+ The robot's policy (its own belief).
159
+ person : TrustAwareHandover
160
+ The simulated person's true trust state (only its belief is used).
161
+ events_rng : numpy.random.Generator
162
+ n_steps : int, optional
163
+
164
+ Returns
165
+ -------
166
+ dict
167
+ ``cost`` (total), ``failures``, ``asks`` and ``actions`` (list).
168
+ """
169
+ total = 0.0
170
+ failures = asks = 0
171
+ actions = []
172
+ for _ in range(n_steps):
173
+ d = policy.decide()
174
+ a = d['action']
175
+ actions.append(a)
176
+ if a == 'ask':
177
+ total += policy.ask_cost
178
+ yes = events_rng.random() < person.p_trust
179
+ person.answer(yes) # asking changes the person's trust too
180
+ policy.answer(yes)
181
+ asks += 1
182
+ continue
183
+ if a == 'wait':
184
+ total += policy.wait_cost
185
+ person.wait()
186
+ policy.wait()
187
+ continue
188
+ p_fail = 1 - person.p_trust
189
+ if a == 'slow_handover':
190
+ total += policy.slow_cost
191
+ p_fail *= policy.slow_factor
192
+ ok = events_rng.random() >= p_fail
193
+ total += 0 if ok else policy.fail_cost
194
+ failures += int(not ok)
195
+ person.observe(int(ok))
196
+ policy.observe(int(ok))
197
+ return {'cost': total, 'failures': failures, 'asks': asks, 'actions': actions}
@@ -0,0 +1,196 @@
1
+ r"""Intent resolution from ambiguous commands and context cues (robots, assistants, interfaces).
2
+
3
+ A user's intent is one of K options ("fetch the red cup", "fetch the blue cup", ...). Each context cue
4
+ (a word, a gesture, a gaze direction, the time of day) carries a likelihood over intents. Classical
5
+ Bayesian updating multiplies the likelihoods, so the order in which cues arrive does not matter.
6
+
7
+ :class:`QuantumIntentResolver` represents belief as amplitudes :math:`\psi = \sqrt{\text{prior}}` and
8
+ applies each cue c as a Kraus operator
9
+
10
+ .. math:: K_c = R(\theta_c)\,\mathrm{diag}\bigl(\sqrt{L_c}\bigr)\,R(\theta_c)^T,
11
+
12
+ where :math:`R(\theta_c)` rotates the intent basis by the cue's incompatibility angle
13
+ :math:`\theta_c`, in the plane of the cue's two most likely intents. With every
14
+ :math:`\theta_c = 0` the posterior is exactly Bayes' rule; :math:`\theta_c \neq 0` makes cues
15
+ incompatible, so their order matters, as in question-order effects. The angles can be fitted to
16
+ observed choices.
17
+
18
+ Examples
19
+ --------
20
+ >>> from quantum_mind.applications.intent import QuantumIntentResolver
21
+ >>> r = QuantumIntentResolver(['red cup', 'blue cup', 'bowl'])
22
+ >>> _ = r.add_cue('points left', [0.70, 0.20, 0.10], theta=0.5)
23
+ >>> _ = r.add_cue('looks right', [0.25, 0.60, 0.15], theta=0.5)
24
+ >>> r.posterior(['points left', 'looks right']).round(3)
25
+ array([0.482, 0.498, 0.02 ])
26
+ >>> round(r.order_effect('points left', 'looks right'), 3)
27
+ 0.079
28
+ """
29
+ from __future__ import annotations
30
+
31
+ import numpy as np
32
+
33
+ __all__ = ['QuantumIntentResolver', 'BayesIntentResolver']
34
+
35
+
36
+ class BayesIntentResolver:
37
+ """Classical baseline: posterior proportional to the prior times the cue likelihoods.
38
+
39
+ Parameters
40
+ ----------
41
+ intents : sequence of str
42
+ Names of the K possible intents.
43
+ prior : array_like, optional
44
+ Prior over intents (normalised); uniform by default.
45
+
46
+ Attributes
47
+ ----------
48
+ cues : dict
49
+ Registered cues, by name.
50
+ """
51
+
52
+ def __init__(self, intents, prior=None):
53
+ self.intents = list(intents)
54
+ K = len(self.intents)
55
+ self.prior = np.full(K, 1 / K) if prior is None else _prob(prior, K)
56
+ self.cues = {}
57
+
58
+ def add_cue(self, name, likelihood, theta=0.0):
59
+ """Register a cue.
60
+
61
+ Parameters
62
+ ----------
63
+ name : str
64
+ Cue name.
65
+ likelihood : array_like
66
+ Non-negative likelihood of the cue under each intent.
67
+ theta : float, optional
68
+ Ignored here (classical cues commute); used by :class:`QuantumIntentResolver`.
69
+
70
+ Returns
71
+ -------
72
+ BayesIntentResolver
73
+ ``self``, so calls can be chained.
74
+ """
75
+ self.cues[name] = _prob(likelihood, len(self.intents), normalise=False)
76
+ return self
77
+
78
+ def posterior(self, cues):
79
+ """Intent probabilities after a sequence of cues.
80
+
81
+ Parameters
82
+ ----------
83
+ cues : sequence of str
84
+ Names of registered cues.
85
+
86
+ Returns
87
+ -------
88
+ numpy.ndarray
89
+ Posterior over intents (independent of the order of ``cues``).
90
+ """
91
+ p = self.prior.copy()
92
+ for c in cues:
93
+ p = p * self.cues[c]
94
+ p = p / p.sum()
95
+ return p
96
+
97
+ def order_effect(self, a, b):
98
+ """Size of the order effect between two cues.
99
+
100
+ Parameters
101
+ ----------
102
+ a, b : str
103
+ Cue names.
104
+
105
+ Returns
106
+ -------
107
+ float
108
+ Total variation distance between the posteriors for orders (a, b) and (b, a); always 0 for
109
+ the Bayesian resolver.
110
+ """
111
+ return 0.5 * float(np.abs(self.posterior([a, b]) - self.posterior([b, a])).sum())
112
+
113
+
114
+ class QuantumIntentResolver(BayesIntentResolver):
115
+ """Quantum-like intent resolver; it reduces to :class:`BayesIntentResolver` when every angle is 0.
116
+
117
+ Parameters
118
+ ----------
119
+ intents : sequence of str
120
+ Names of the K possible intents.
121
+ prior : array_like, optional
122
+ Prior over intents; the initial amplitudes are its square roots.
123
+ """
124
+
125
+ def add_cue(self, name, likelihood, theta=0.0):
126
+ """Register a cue as a Kraus operator.
127
+
128
+ Parameters
129
+ ----------
130
+ name : str
131
+ Cue name.
132
+ likelihood : array_like
133
+ Non-negative likelihood of the cue under each intent.
134
+ theta : float, optional
135
+ Incompatibility angle in radians (0: the cue commutes with every other cue).
136
+
137
+ Returns
138
+ -------
139
+ QuantumIntentResolver
140
+ ``self``.
141
+ """
142
+ L = _prob(likelihood, len(self.intents), normalise=False)
143
+ i, j = np.argsort(L)[::-1][:2] # rotate in the plane of the two leading intents
144
+ R = np.eye(len(L))
145
+ c, s = np.cos(theta), np.sin(theta)
146
+ R[[i, i, j, j], [i, j, i, j]] = [c, -s, s, c]
147
+ self.cues[name] = R @ np.diag(np.sqrt(L)) @ R.T
148
+ return self
149
+
150
+ def posterior(self, cues):
151
+ """Intent probabilities :math:`|\\psi|^2` after applying the cues in order.
152
+
153
+ Parameters
154
+ ----------
155
+ cues : sequence of str
156
+ Names of registered cues, in the order they arrived.
157
+
158
+ Returns
159
+ -------
160
+ numpy.ndarray
161
+ """
162
+ psi = np.sqrt(self.prior)
163
+ for c in cues:
164
+ psi = self.cues[c] @ psi
165
+ psi = psi / np.linalg.norm(psi)
166
+ return psi ** 2
167
+
168
+ def resolve(self, cues):
169
+ """Most likely intent after the cues.
170
+
171
+ Parameters
172
+ ----------
173
+ cues : sequence of str
174
+ Names of registered cues, in order.
175
+
176
+ Returns
177
+ -------
178
+ intent : str
179
+ Most likely intent.
180
+ probability : float
181
+ Its posterior probability.
182
+ entropy : float
183
+ Entropy of the posterior in bits (high values suggest asking for clarification).
184
+ """
185
+ p = self.posterior(cues)
186
+ k = int(np.argmax(p))
187
+ H = float(-np.sum(p[p > 0] * np.log2(p[p > 0])))
188
+ return self.intents[k], float(p[k]), H
189
+
190
+
191
+ def _prob(v, K, normalise=True):
192
+ """Validate K non-negative weights and optionally normalise them."""
193
+ v = np.asarray(v, float)
194
+ if v.shape != (K,) or np.any(v < 0) or v.sum() <= 0:
195
+ raise ValueError('expected %d non-negative weights' % K)
196
+ return v / v.sum() if normalise else v