Skip to content

Commit 14fddc6

Browse files
CopilotMoritzWillmannCopilot
committed
Enforce signature-only parameters in get_params/set_params across all squlearn classes (#381)
* Initial plan * Add read-only properties for encoding_circuit_str, feature_str, and parameter_str Co-authored-by: MoritzWillmann <44642314+MoritzWillmann@users.noreply.github.com> * Remove encoding_circuit_str from get_params and add properties for signature params in LayeredEncodingCircuit and ParamZFeatureMap Co-authored-by: MoritzWillmann <44642314+MoritzWillmann@users.noreply.github.com> * Add read-only properties to encoding circuit classes - ChebyshevRx: Added properties for num_layers, closed, alpha, nonlinearity - ChebyshevPQC: Added properties for num_layers, closed, entangling_gate, alpha, nonlinearity - KyriienkoEncodingCircuit: Added properties for num_encoding_layers, num_variational_layers, variational_arrangement, encoding_style, block_width, block_depth, rotation_gate - Changed instance variables to use underscore prefix (_param) for properties - Properties placed before get_params() method with proper type hints and docstrings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add read-only properties to HubregtsenEncodingCircuit for signature parameters Co-authored-by: MoritzWillmann <44642314+MoritzWillmann@users.noreply.github.com> * Add read-only properties to Observable classes - Changed attribute assignments in __init__ to use underscore prefix - Added @Property decorators for read-only access to parameters - Updated all internal references to use underscore-prefixed attributes - Properties placed before get_params() method - Follows pattern from HubregtsenEncodingCircuit Files updated: - single_probability.py: qubit, one_state, parameterized - custom_observable.py: operator_string, parameterized - summed_probabilities.py: one_state, full_sum, include_identity - ising_hamiltonian.py: I, Z, X, ZZ - summed_paulis.py: op_str, full_sum, include_identity - single_pauli.py: qubit, op_str, parameterized All existing tests pass. set_params() works correctly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add read-only properties to encoding circuit classes - Add read-only properties to MultiControlEncodingCircuit (num_layers, closed, final_encoding) - Add read-only properties to ChebyshevTower (num_chebyshev, alpha, num_layers, rotation_gate, hadamard_start, arrangement, nonlinearity) - Add read-only properties to RandomLayeredEncodingCircuit (seed, min_num_layers, max_num_layers, feature_probability) - Add read-only properties to HighDimEncodingCircuit (cycling, cycling_type, num_layers, layer_type, entangling_gate) All properties follow the same pattern as HubregtsenEncodingCircuit: - Use underscore prefix for internal storage - Provide read-only access via @Property decorators - Maintain backward compatibility with get_params/set_params All existing tests pass successfully. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Clean up temporary test files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Document lazy initialization pattern in HighDimEncodingCircuit Added detailed documentation to the num_layers property explaining the lazy initialization behavior. While the property is read-only from an external perspective (users cannot directly assign to it), it is internally mutable to support the lazy initialization pattern where num_layers=None is auto-determined based on features during get_circuit(). This design is intentional and tested for backward compatibility. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix docstring for HighDimEncodingCircuit.num_layers property Co-authored-by: MoritzWillmann <44642314+MoritzWillmann@users.noreply.github.com> * Fix black formatting issues in highdim_encoding_circuit.py and test_layered_encoding_circuit.py Co-authored-by: MoritzWillmann <44642314+MoritzWillmann@users.noreply.github.com> * Add read-only properties to QGPR and QKRR for signature parameters Co-authored-by: MoritzWillmann <44642314+MoritzWillmann@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: MoritzWillmann <44642314+MoritzWillmann@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 566308b commit 14fddc6

19 files changed

Lines changed: 676 additions & 325 deletions

src/squlearn/encoding_circuit/circuit_library/chebyshev_pqc.py

Lines changed: 58 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -70,27 +70,27 @@ def __init__(
7070
nonlinearity: str = "arccos",
7171
) -> None:
7272
super().__init__(num_qubits, num_features)
73-
self.num_layers = num_layers
74-
self.closed = closed
75-
self.entangling_gate = entangling_gate
76-
self.alpha = alpha
77-
self.nonlinearity = nonlinearity
78-
if self.entangling_gate not in ("crz", "rzz"):
73+
self._num_layers = num_layers
74+
self._closed = closed
75+
self._entangling_gate = entangling_gate
76+
self._alpha = alpha
77+
self._nonlinearity = nonlinearity
78+
if self._entangling_gate not in ("crz", "rzz"):
7979
raise ValueError("Unknown value for entangling_gate: ", entangling_gate)
80-
if self.nonlinearity not in ("arccos", "arctan"):
80+
if self._nonlinearity not in ("arccos", "arctan"):
8181
raise ValueError(
82-
f"Unknown value for nonlinearity: {self.nonlinearity}."
82+
f"Unknown value for nonlinearity: {self._nonlinearity}."
8383
" Possible values are 'arccos' and 'arctan'"
8484
)
8585

8686
@property
8787
def num_parameters(self) -> int:
8888
"""The number of trainable parameters of the ChebyshevPQC encoding circuit."""
89-
num_param = 2 * self.num_qubits + self.num_qubits * self.num_layers
90-
if self.num_qubits > 2 and self.closed:
91-
num_param += self.num_qubits * self.num_layers
89+
num_param = 2 * self.num_qubits + self.num_qubits * self._num_layers
90+
if self.num_qubits > 2 and self._closed:
91+
num_param += self.num_qubits * self._num_layers
9292
else:
93-
num_param += (self.num_qubits - 1) * self.num_layers
93+
num_param += (self.num_qubits - 1) * self._num_layers
9494
return num_param
9595

9696
@property
@@ -104,18 +104,18 @@ def parameter_bounds(self) -> np.ndarray:
104104
bounds[index_offset] = [-np.pi, np.pi]
105105
index_offset += 1
106106

107-
for layer in range(self.num_layers):
107+
for layer in range(self._num_layers):
108108
# Chebyshev encoding circuit
109109
for i in range(self.num_qubits):
110-
bounds[index_offset] = [0.0, self.alpha]
110+
bounds[index_offset] = [0.0, self._alpha]
111111
index_offset += 1
112112

113113
for i in range(0, self.num_qubits, 2):
114114
bounds[index_offset] = [-2.0 * np.pi, 2.0 * np.pi]
115115
index_offset += 1
116116

117117
if self.num_qubits > 2:
118-
if self.closed:
118+
if self._closed:
119119
istop = self.num_qubits
120120
else:
121121
istop = self.num_qubits - 1
@@ -147,7 +147,7 @@ def generate_initial_parameters(
147147
if len(param) > 0:
148148
index = self.get_cheb_indices(False)
149149
features_per_qubit = int(np.ceil(self.num_qubits / num_features))
150-
p = np.linspace(0.01, self.alpha, features_per_qubit)
150+
p = np.linspace(0.01, self._alpha, features_per_qubit)
151151

152152
for index2 in index:
153153
for i, ii in enumerate(index2):
@@ -162,15 +162,40 @@ def feature_bounds(self) -> np.ndarray:
162162
163163
To get the bounds for a specific number of features, use get_feature_bounds().
164164
"""
165-
if self.nonlinearity == "arccos":
165+
if self._nonlinearity == "arccos":
166166
return np.array([-1.0, 1.0])
167-
elif self.nonlinearity == "arctan":
167+
elif self._nonlinearity == "arctan":
168168
return np.array([-np.inf, np.inf])
169169

170170
@property
171171
def num_encoding_slots(self) -> int:
172172
"""The number of encoding slots of the ChebyshevPQC encoding circuit."""
173-
return self.num_qubits * self.num_layers
173+
return self.num_qubits * self._num_layers
174+
175+
@property
176+
def num_layers(self) -> int:
177+
"""The number of layers of the encoding circuit."""
178+
return self._num_layers
179+
180+
@property
181+
def closed(self) -> bool:
182+
"""Whether the last and the first qubit are entangled."""
183+
return self._closed
184+
185+
@property
186+
def entangling_gate(self) -> str:
187+
"""The entangling gate to use."""
188+
return self._entangling_gate
189+
190+
@property
191+
def alpha(self) -> float:
192+
"""The maximum value of the Chebyshev Tower initial parameters."""
193+
return self._alpha
194+
195+
@property
196+
def nonlinearity(self) -> str:
197+
"""The mapping function for the feature encoding."""
198+
return self._nonlinearity
174199

175200
def get_params(self, deep: bool = True) -> dict:
176201
"""
@@ -184,11 +209,11 @@ def get_params(self, deep: bool = True) -> dict:
184209
Dictionary with hyper-parameters and values.
185210
"""
186211
params = super().get_params()
187-
params["num_layers"] = self.num_layers
188-
params["closed"] = self.closed
189-
params["entangling_gate"] = self.entangling_gate
190-
params["alpha"] = self.alpha
191-
params["nonlinearity"] = self.nonlinearity
212+
params["num_layers"] = self._num_layers
213+
params["closed"] = self._closed
214+
params["entangling_gate"] = self._entangling_gate
215+
params["alpha"] = self._alpha
216+
params["nonlinearity"] = self._nonlinearity
192217

193218
return params
194219

@@ -220,7 +245,7 @@ def get_cheb_indices(self, flatten: bool = True):
220245
"""
221246
cheb_index = []
222247
index_offset = self.num_qubits
223-
for layer in range(self.num_layers):
248+
for layer in range(self._num_layers):
224249
cheb_index_layer = []
225250
for i in range(self.num_qubits):
226251
cheb_index_layer.append(index_offset)
@@ -230,7 +255,7 @@ def get_cheb_indices(self, flatten: bool = True):
230255
index_offset += 1
231256

232257
if self.num_qubits > 2:
233-
if self.closed:
258+
if self._closed:
234259
istop = self.num_qubits
235260
else:
236261
istop = self.num_qubits - 1
@@ -260,13 +285,13 @@ def get_circuit(
260285
Returns the circuit in Qiskit's QuantumCircuit format
261286
"""
262287

263-
if self.nonlinearity == "arccos":
288+
if self._nonlinearity == "arccos":
264289

265290
def mapping(a, x):
266291
"""Helper function for returning a*arccos(x)"""
267292
return a * np.arccos(x)
268293

269-
elif self.nonlinearity == "arctan":
294+
elif self._nonlinearity == "arctan":
270295

271296
def mapping(a, x):
272297
"""Helper function for returning a*arctan(x)"""
@@ -281,9 +306,9 @@ def mapping(a, x):
281306
index_offset = 0
282307
feature_offset = 0
283308

284-
if self.entangling_gate == "crz":
309+
if self._entangling_gate == "crz":
285310
egate = QC.crz
286-
elif self.entangling_gate == "rzz":
311+
elif self._entangling_gate == "rzz":
287312
egate = QC.rzz
288313
else:
289314
raise ValueError("Unknown entangling gate")
@@ -293,7 +318,7 @@ def mapping(a, x):
293318
QC.ry(parameters[index_offset % num_params], i)
294319
index_offset += 1
295320

296-
for _ in range(self.num_layers):
321+
for _ in range(self._num_layers):
297322
# Chebyshev encoding circuit
298323
for i in range(self.num_qubits):
299324
QC.rx(
@@ -306,12 +331,12 @@ def mapping(a, x):
306331
index_offset += 1
307332
feature_offset += 1
308333

309-
for i in range(0, self.num_qubits + self.closed - 1, 2):
334+
for i in range(0, self.num_qubits + self._closed - 1, 2):
310335
egate(parameters[index_offset % num_params], i, (i + 1) % self.num_qubits)
311336
index_offset += 1
312337

313338
if self.num_qubits > 2:
314-
for i in range(1, self.num_qubits + self.closed - 1, 2):
339+
for i in range(1, self.num_qubits + self._closed - 1, 2):
315340
egate(parameters[index_offset % num_params], i, (i + 1) % self.num_qubits)
316341
index_offset += 1
317342

src/squlearn/encoding_circuit/circuit_library/chebyshev_rx.py

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -45,30 +45,30 @@ def __init__(
4545
nonlinearity: str = "arccos",
4646
) -> None:
4747
super().__init__(num_qubits, num_features)
48-
self.num_layers = num_layers
49-
self.closed = closed
50-
self.alpha = alpha
51-
self.nonlinearity = nonlinearity
52-
if self.nonlinearity not in ("arccos", "arctan"):
48+
self._num_layers = num_layers
49+
self._closed = closed
50+
self._alpha = alpha
51+
self._nonlinearity = nonlinearity
52+
if self._nonlinearity not in ("arccos", "arctan"):
5353
raise ValueError(
54-
f"Unknown value for nonlinearity: {self.nonlinearity}."
54+
f"Unknown value for nonlinearity: {self._nonlinearity}."
5555
" Possible values are 'arccos' and 'arctan'"
5656
)
5757

5858
@property
5959
def num_parameters(self) -> int:
6060
"""The number of trainable parameters of the ChebyshevRx encoding circuit."""
61-
return 2 * self.num_qubits * self.num_layers
61+
return 2 * self.num_qubits * self._num_layers
6262

6363
@property
6464
def parameter_bounds(self) -> np.ndarray:
6565
"""The bounds of the trainable parameters of the ChebyshevRx encoding circuit."""
6666
bounds = np.zeros((self.num_parameters, 2))
6767
index_offset = 0
68-
for layer in range(self.num_layers):
68+
for layer in range(self._num_layers):
6969
# Chebyshev encoding circuit
7070
for i in range(self.num_qubits):
71-
bounds[index_offset] = [0.0, self.alpha]
71+
bounds[index_offset] = [0.0, self._alpha]
7272
index_offset += 1
7373
# Trafo
7474
for i in range(self.num_qubits):
@@ -94,7 +94,7 @@ def generate_initial_parameters(
9494
if len(param) > 0:
9595
index = self.get_cheb_indices(False)
9696
features_per_qubit = int(np.ceil(self.num_qubits / num_features))
97-
p = np.linspace(0.01, self.alpha, features_per_qubit)
97+
p = np.linspace(0.01, self._alpha, features_per_qubit)
9898

9999
for index2 in index:
100100
for i, ii in enumerate(index2):
@@ -109,15 +109,35 @@ def feature_bounds(self) -> np.ndarray:
109109
110110
To get the bounds for a specific number of features, use get_feature_bounds().
111111
"""
112-
if self.nonlinearity == "arccos":
112+
if self._nonlinearity == "arccos":
113113
return np.array([-1.0, 1.0])
114-
elif self.nonlinearity == "arctan":
114+
elif self._nonlinearity == "arctan":
115115
return np.array([-np.inf, np.inf])
116116

117117
@property
118118
def num_encoding_slots(self) -> int:
119119
"""The number of encoding slots of the ChebyshevRx encoding circuit."""
120-
return self.num_layers * self.num_qubits
120+
return self._num_layers * self.num_qubits
121+
122+
@property
123+
def num_layers(self) -> int:
124+
"""The number of layers of the encoding circuit."""
125+
return self._num_layers
126+
127+
@property
128+
def closed(self) -> bool:
129+
"""Whether the last and the first qubit are entangled."""
130+
return self._closed
131+
132+
@property
133+
def alpha(self) -> float:
134+
"""The maximum value of the Chebyshev Tower initial parameters."""
135+
return self._alpha
136+
137+
@property
138+
def nonlinearity(self) -> str:
139+
"""The mapping function for the feature encoding."""
140+
return self._nonlinearity
121141

122142
def get_params(self, deep: bool = True) -> dict:
123143
"""
@@ -131,10 +151,10 @@ def get_params(self, deep: bool = True) -> dict:
131151
Dictionary with hyper-parameters and values.
132152
"""
133153
params = super().get_params()
134-
params["num_layers"] = self.num_layers
135-
params["closed"] = self.closed
136-
params["alpha"] = self.alpha
137-
params["nonlinearity"] = self.nonlinearity
154+
params["num_layers"] = self._num_layers
155+
params["closed"] = self._closed
156+
params["alpha"] = self._alpha
157+
params["nonlinearity"] = self._nonlinearity
138158
return params
139159

140160
def set_params(self, **kwargs) -> ChebyshevRx:
@@ -165,7 +185,7 @@ def get_cheb_indices(self, flatten: bool = True):
165185
"""
166186
cheb_index = []
167187
index_offset = 0
168-
for layer in range(self.num_layers):
188+
for layer in range(self._num_layers):
169189
cheb_index_layer = []
170190
for i in range(self.num_qubits):
171191
cheb_index_layer.append(index_offset)
@@ -204,22 +224,22 @@ def get_circuit(
204224

205225
def entangle_layer(QC: QuantumCircuit) -> QuantumCircuit:
206226
"""Creation of a simple nearest neighbor entangling layer"""
207-
for i in range(0, self.num_qubits + self.closed - 1, 2):
227+
for i in range(0, self.num_qubits + self._closed - 1, 2):
208228
QC.cx(i, (i + 1) % self.num_qubits)
209229

210230
if self.num_qubits > 2:
211-
for i in range(1, self.num_qubits + self.closed - 1, 2):
231+
for i in range(1, self.num_qubits + self._closed - 1, 2):
212232
QC.cx(i, (i + 1) % self.num_qubits)
213233

214234
return QC
215235

216-
if self.nonlinearity == "arccos":
236+
if self._nonlinearity == "arccos":
217237

218238
def mapping(a, x):
219239
"""Helper function for returning a*arccos(x)"""
220240
return a * np.arccos(x)
221241

222-
elif self.nonlinearity == "arctan":
242+
elif self._nonlinearity == "arctan":
223243

224244
def mapping(a, x):
225245
"""Helper function for returning a*arctan(x)"""
@@ -228,7 +248,7 @@ def mapping(a, x):
228248
QC = QuantumCircuit(self.num_qubits)
229249
index_offset = 0
230250
feature_offset = 0
231-
for _ in range(self.num_layers):
251+
for _ in range(self._num_layers):
232252
# Chebyshev encoding circuit
233253
for i in range(self.num_qubits):
234254
QC.rx(

0 commit comments

Comments
 (0)