From c3ec8ac737e1eaa73b4c23776b016751e07d5c0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=9A=E5=AF=85?= <13017899+hefei-wu-yanzu@user.noreply.gitee.com> Date: Wed, 20 Aug 2025 02:13:38 +0000 Subject: [PATCH 01/19] update pyproject.toml. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 阚寅 <13017899+hefei-wu-yanzu@user.noreply.gitee.com> --- pyproject.toml | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1a2edfb..467ec29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,10 @@ authors = [ readme = "README.md" requires-python = ">=3.10" license = "Apache-2.0" -dynamic = ["version", "dependencies"] +dependencies = [ + "pennylane>=0.36.0", + "cqlib" +] classifiers = [ "Environment :: Console", "Operating System :: MacOS", @@ -24,6 +27,7 @@ classifiers = [ keywords = [ "cqlib", "cqlib adapter", + "pennylane", "quantum circuit", "quantum computing", "quantum programming language", @@ -31,13 +35,16 @@ keywords = [ "sdk", ] +[project.entry-points."pennylane.plugins"] +"cqlib.device" = "cqlib_adapter.pennylane_ext.device:CQLibDevice" + [tool.setuptools] -packages = ["cqlib_adapter", "cqlib_adapter.qiskit_ext"] +packages = ["cqlib_adapter", "cqlib_adapter.qiskit_ext", "cqlib_adapter.pennylane_ext"] +include-package-data = true -[tool.setuptools.dynamic] -version = { file = "cqlib_adapter/VERSION.txt" } -dependencies = { file = "requirements.txt" } +[tool.setuptools.package-data] +"cqlib_adapter.pennylane_ext" = ["cqlib_config.toml"] [build-system] requires = ["setuptools>=77"] -build-backend = "setuptools.build_meta" +build-backend = "setuptools.build_meta" \ No newline at end of file -- Gitee From fd17d00395ebf987e416926c3f3b0861db5694f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=9A=E5=AF=85?= <13017899+hefei-wu-yanzu@user.noreply.gitee.com> Date: Wed, 20 Aug 2025 02:15:12 +0000 Subject: [PATCH 02/19] =?UTF-8?q?=E6=96=B0=E5=BB=BA=20pennylane=5Fext?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cqlib_adapter/pennylane_ext/.keep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 cqlib_adapter/pennylane_ext/.keep diff --git a/cqlib_adapter/pennylane_ext/.keep b/cqlib_adapter/pennylane_ext/.keep new file mode 100644 index 0000000..e69de29 -- Gitee From 088caefb0d54d6600ef4bac4ade2ae47cad2f794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=9A=E5=AF=85?= <13017899+hefei-wu-yanzu@user.noreply.gitee.com> Date: Wed, 20 Aug 2025 02:16:04 +0000 Subject: [PATCH 03/19] =?UTF-8?q?=E7=AC=AC=E4=B8=80=E6=AC=A1=E6=9B=B4?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 阚寅 <13017899+hefei-wu-yanzu@user.noreply.gitee.com> --- cqlib_adapter/pennylane_ext/__init__.py | 3 + cqlib_adapter/pennylane_ext/cqlib_config.toml | 37 ++ cqlib_adapter/pennylane_ext/device.py | 336 ++++++++++++++++++ 3 files changed, 376 insertions(+) create mode 100644 cqlib_adapter/pennylane_ext/__init__.py create mode 100644 cqlib_adapter/pennylane_ext/cqlib_config.toml create mode 100644 cqlib_adapter/pennylane_ext/device.py diff --git a/cqlib_adapter/pennylane_ext/__init__.py b/cqlib_adapter/pennylane_ext/__init__.py new file mode 100644 index 0000000..2975fd1 --- /dev/null +++ b/cqlib_adapter/pennylane_ext/__init__.py @@ -0,0 +1,3 @@ +from .device import CQLibDevice + +__all__ = ['CQLibDevice'] \ No newline at end of file diff --git a/cqlib_adapter/pennylane_ext/cqlib_config.toml b/cqlib_adapter/pennylane_ext/cqlib_config.toml new file mode 100644 index 0000000..418a7fc --- /dev/null +++ b/cqlib_adapter/pennylane_ext/cqlib_config.toml @@ -0,0 +1,37 @@ +schema = 3 + +[operators.gates] +PauliX = { properties = ["controllable", "invertible"] } +PauliY = { properties = ["controllable", "invertible"] } +PauliZ = { properties = ["controllable", "invertible"] } +Hadamard = { properties = ["controllable", "invertible"] } +S = { properties = ["controllable", "invertible"] } +T = { properties = ["controllable", "invertible"] } + +RX = { properties = ["controllable", "invertible", "differentiable"] } +RY = { properties = ["controllable", "invertible", "differentiable"] } +RZ = { properties = ["controllable", "invertible", "differentiable"] } + +CNOT = { properties = ["invertible"] } +CZ = { properties = ["invertible"] } + + +[operators.observables] +PauliX = {} +PauliY = {} +PauliZ = {} + + +[measurement_processes] +ExpectationMP = {} +SampleMP = { conditions = ["finiteshots"] } +CountsMP = { conditions = ["finiteshots"] } + +[compilation] +qjit_compatible = false +runtime_code_generation = false +dynamic_qubit_management = false +overlapping_observables = true +non_commuting_observables = false +initial_state_prep = false +supported_mcm_methods = [] diff --git a/cqlib_adapter/pennylane_ext/device.py b/cqlib_adapter/pennylane_ext/device.py new file mode 100644 index 0000000..fbde29a --- /dev/null +++ b/cqlib_adapter/pennylane_ext/device.py @@ -0,0 +1,336 @@ +import pennylane as qml +from pennylane.devices import Device, DefaultExecutionConfig +from pennylane.tape import QuantumScriptOrBatch +import numpy as np +import cqlib +from cqlib import TianYanPlatform +import json +from cqlib.utils import qasm2 +from os import path +from cqlib.mapping import transpile_qcis +from cqlib.simulator import SimpleSimulator, StatevectorSimulator +from typing import Union, List, Dict +from pennylane.tape import QuantumScript +from pennylane.gradients import param_shift + +login_key = "ZtQYpi6GVW24lrSOpauj16mRCAFrWN/3Et4xJjhn7dg=" +class CQLibDevice(Device): + short_name = 'cqlib.device' + pennylane_requires = '0.42.1' + version = '0.1.0' + author = 'Ky' + config_filepath = path.join(path.dirname(__file__), "cqlib_config.toml") + + def __init__(self, wires, shots=None, cqlib_backend_name="default", **kwargs): + super().__init__(wires=wires, shots=shots) + self.num_wires = wires + self.num_shots = shots + self.machine_name = cqlib_backend_name + + self.cqlib_backend = TianYanPlatform(login_key=login_key, machine_name=cqlib_backend_name) + + + # self._gradient_transform = param_shift + # self._gradient_kwargs = {} + def name(self): + return 'CQLib Quantum Device' + @classmethod + def capabilities(cls): + # 获取父类能力字典 + capabilities = super().capabilities().copy() # 必须创建副本以避免修改父类属性 + + # 更新设备特定能力 + capabilities.update( + model="qubit", # 或 "cv" (连续变量) + supports_inverse_operations=True, + supports_analytic_computation=True, + supports_finite_shots=True, + + returns_state=False, + passthru_devices={ + "autograd": "default.qubit.autograd", + "tf": "default.qubit.tf", + "torch": "default.qubit.torch", + "jax": "default.qubit.jax", + }, + + # ✅ 新增梯度相关字段 + supports_derivatives=True, # 表明支持梯度 + grad_methods={"device": True} # 支持 diff_method="device" + ) + + return capabilities + def execute(self, circuits: QuantumScriptOrBatch, execution_config=None): + """Execute quantum circuits on the target backend. + + Args: + circuits: Quantum circuit(s) to execute + execution_config: Execution configuration + + Returns: + List of results corresponding to each circuit's measurements + """ + # Ensure circuits is a list even if single circuit is provided + circuits = [circuits] if isinstance(circuits, qml.tape.QuantumScript) else circuits + results = [] + + for circuit in circuits: + # Convert circuit to QCIS format + qasm_str = circuit.to_openqasm() + cqlib_cir = qasm2.loads(qasm_str) + cqlib_qcis = cqlib_cir.qcis + print(qasm_str) + + # 根据后端类型选择执行方式 + # 此处约定将每一种后端的计算结果都调整为一个dict或者dict的list,方便统一处理 + if self._is_tianyan_hardware(): + compiled_circuit = transpile_qcis(cqlib_qcis, self.cqlib_backend) + query_id = self.cqlib_backend.submit_experiment(compiled_circuit[0].qcis, num_shots=self.num_shots) + raw_result = self.cqlib_backend.query_experiment(query_id, readout_calibration=True) + res = extract_probability(raw_result, num_wires=self.num_wires) + # print(res) + + elif self._is_tianyan_simulator(): + query_id = self.cqlib_backend.submit_experiment(cqlib_qcis, num_shots=self.num_shots) + raw_result = self.cqlib_backend.query_experiment(query_id) + res = extract_probability(raw_result, num_wires=self.num_wires) + # print(res) + else: + # caution: 本地模拟器只接受线路对象,不是qcis串 + res = self._execute_on_simulator(cqlib_cir) + # print(res) + + # Process measurements + circuit_results = self._process_measurements(circuit, res) + results.append(circuit_results) + + return results + + def _is_tianyan_hardware(self): + """Check if the current backend is Tianyan hardware.""" + return self.machine_name in {"tianyan24", "tianyan504", "tianyan176-2", "tianyan176"} + + def _is_tianyan_simulator(self): + """Check if the current backend is Tianyan simulator.""" + return self.machine_name in {"tianyan_sw", "tianyan_s", "tianyan_tn", "tianyan_tnn", "tianyan_sa", "tianyan_swn"} + + def _execute_on_simulator(self, circuit): + """Execute circuit on local simulator.""" + + # simulator = SimpleSimulator(circuit, device="cpu") + simulator = StatevectorSimulator(circuit) + return simulator.sample() + + def _process_measurements(self, circuit: QuantumScript, raw_result: Union[dict, List[dict]]) -> Union[float, np.ndarray, List[Union[float, np.ndarray]]]: + """ + Process measurements based on circuit's measurement operations. + + Args: + circuit: PennyLane quantum circuit. + raw_result: Raw result from the backend (simulator or hardware). + + Returns: + Measurement results (probabilities or expectations). + """ + results = [] + for meas in circuit.measurements: + if isinstance(meas, qml.measurements.ExpectationMP): + results.append(self._process_expectation(meas, raw_result)) + elif isinstance(meas, qml.measurements.ProbabilityMP): + results.append(self._process_probability(meas, raw_result)) + else: + raise NotImplementedError(f"Measurement type {type(meas).__name__} is not supported") + return results[0] if len(results) == 1 else results + + + def _process_expectation(self, meas, raw_result) -> float: + """Process expectation value measurement.""" + # 目前只支持 PauliZ 的期望 + if meas.obs.name != "PauliZ": + raise NotImplementedError(f"Expectation for {meas.obs.name} is not supported") + + # 使用 local 或 hardware 解析 + if isinstance(raw_result, list): + return self.process_results(raw_result) + elif isinstance(raw_result, dict): + local_exp = self.process_results_local(raw_result) + return local_exp[meas.wires[0]] + else: + raise ValueError(f"Unsupported raw_result type: {type(raw_result)}") + + + def _process_probability(self, meas, raw_result) -> np.ndarray: + """Process probability measurement.""" + num_wires = len(meas.wires) + probs = np.zeros(2**num_wires) + + if isinstance(raw_result, dict): # 本地 simulator + total_shots = sum(raw_result.values()) + for bitstring, count in raw_result.items(): + index = int(bitstring[::-1], 2) + probs[index] = count / total_shots + elif isinstance(raw_result, list): # 硬件或云 simulator + try: + prob_dict = json.loads(raw_result[0]["probability"]) + for bitstring, prob in prob_dict.items(): + index = int(bitstring[::-1], 2) + probs[index] = prob + except (json.JSONDecodeError, KeyError, TypeError) as e: + raise ValueError(f"Invalid raw_result format: {e}") + else: + raise ValueError(f"Unsupported raw_result type: {type(raw_result)}") + + # 检查概率和 + if not np.isclose(np.sum(probs), 1.0, rtol=1e-5): + raise ValueError(f"Probabilities do not sum to 1: {np.sum(probs)}") + + return probs + def process_results(self, raw_result): + # 解析概率分布(例如:{"00":0.788, "11":0.044, ...}) + prob_dict = json.loads(raw_result[0]["probability"]) + + expectation = 0.0 + for state, prob in prob_dict.items(): + # 计算第一个量子比特的 Z 期望值 + if state[0] == "0": # |0⟩ 对应 Z 的本征值 +1 + expectation += prob + else: # |1⟩ 对应 Z 的本征值 -1 + expectation -= prob + + return expectation + def process_results_local(self, raw_result): + total_shots = sum(raw_result.values()) + + # 初始化每个量子比特的计数 + z_expectations = {} + num_qubits = len(next(iter(raw_result.keys()))) # 获取比特串长度(这里是2个量子比特) + + for qubit in range(num_qubits): + n0, n1 = 0, 0 + for bitstring, count in raw_result.items(): + # 检查当前量子比特的值(注意比特串顺序可能是小端序) + bit = int(bitstring[-qubit - 1]) # 假设右侧是qubit0,左侧是qubit1 + if bit == 0: + n0 += count + else: + n1 += count + z_expectations[qubit] = (n0 - n1) / total_shots + + return z_expectations + + def preprocess_transforms(self, execution_config=None): + program = qml.transforms.core.TransformProgram() + program.add_transform(qml.devices.preprocess.validate_device_wires, wires=self.wires, name=self.short_name) + program.add_transform(qml.devices.preprocess.validate_measurements, name=self.short_name) + program.add_transform(qml.devices.preprocess.decompose, stopping_condition=self.supports_operation, name=self.short_name) + return program + + def supports_operation(self, op): + supported_ops = { + "PauliX", "PauliY", "PauliZ", "Hadamard", "S", "T", + "RX", "RY", "RZ", "CNOT", "CZ" + } + return getattr(op, "name", None) in supported_ops + + def __repr__(self): + return f"<{self.name()} device (wires={self.num_wires}, shots={self.shots})>" + + def gradient(self, tape, **kwargs): + """ + 输入: + tape: QNode 的量子操作序列 + 返回: + 对每个可微参数的梯度 + """ + grads = [] + # 遍历 tape 中的参数 + for op in tape.operations: + for i, param in enumerate(op.parameters): + # 这里用参数移位法举例 + shift = np.pi/2 + op.parameters[i] += shift + forward_plus = self.execute(tape) + op.parameters[i] -= 2*shift + forward_minus = self.execute(tape) + op.parameters[i] += shift # 复原 + + grad = (forward_plus - forward_minus) / 2.0 + grads.append(grad) + return np.array(grads) + + def compute_derivatives(self, circuits: QuantumScriptOrBatch): + if isinstance(circuits, qml.tape.QuantumScript): + circuits = [circuits] + + gradients = [] + for circuit in circuits: + params = circuit.get_parameters() + grad = np.zeros(len(params)) + + for i in range(len(params)): + # 创建参数移位后的电路 + shifted_plus = circuit.copy() + shifted_minus = circuit.copy() + + # 正确设置参数 + new_params_plus = params.copy() + new_params_minus = params.copy() + new_params_plus[i] += np.pi/2 + new_params_minus[i] -= np.pi/2 + + shifted_plus.set_parameters(new_params_plus) + shifted_minus.set_parameters(new_params_minus) + + # 执行移位电路 + res_plus = self.execute(shifted_plus)[0] # 确保获取标量结果 + res_minus = self.execute(shifted_minus)[0] + + # 标准参数移位公式 + grad[i] = (res_plus - res_minus) / 2 + + gradients.append(grad) + + return gradients[0] if len(gradients) == 1 else gradients + def supports_operation(self, op): + supported_ops = {"RX", "RY", "CNOT"} # 根据硬件调整 + return getattr(op, "name", None) in supported_ops + + def supports_derivatives(self, execution_config=None, circuit=None): + """声明设备支持导数计算""" + return False + + def __repr__(self): + return f"<{self.name()} device (wires={self.num_wires}, shots={self.shots})>" + + +def extract_probability(json_data: List[Dict[str, Union[Dict[str, float], list]]], num_wires: int) -> Dict: + """ + Extract probability distribution from JSON data. + + Args: + json_data: JSON data containing measurement results, expected as a list with a dictionary + containing a 'probability' field. + num_wires: Number of qubits (wires) in the circuit. + + Returns: + Dict: Probability distribution for each qubit. + Raises: + ValueError: If JSON data is invalid or probability field is missing/invalid. + """ + if not isinstance(json_data, list) or not json_data: + raise ValueError("json_data must be a non-empty list") + if not isinstance(json_data[0], dict): + raise ValueError("json_data[0] must be a dictionary") + if "probability" not in json_data[0]: + raise ValueError("probability field missing in json_data[0]") + + try: + prob_dict = json_data[0]["probability"] + if isinstance(prob_dict, str): + prob_dict = json.loads(prob_dict) + if not isinstance(prob_dict, dict): + raise ValueError("probability field must be a dictionary") + except (KeyError, TypeError) as e: + raise ValueError(f"Invalid probability field format: {e}") + + return prob_dict \ No newline at end of file -- Gitee From 670ed72c49f4ee0423ed2f5181d3b20e133bbd75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=9A=E5=AF=85?= <13017899+hefei-wu-yanzu@user.noreply.gitee.com> Date: Wed, 20 Aug 2025 07:11:04 +0000 Subject: [PATCH 04/19] update cqlib_adapter/pennylane_ext/device.py. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 阚寅 <13017899+hefei-wu-yanzu@user.noreply.gitee.com> --- cqlib_adapter/pennylane_ext/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlib_adapter/pennylane_ext/device.py b/cqlib_adapter/pennylane_ext/device.py index fbde29a..50d567d 100644 --- a/cqlib_adapter/pennylane_ext/device.py +++ b/cqlib_adapter/pennylane_ext/device.py @@ -13,7 +13,7 @@ from typing import Union, List, Dict from pennylane.tape import QuantumScript from pennylane.gradients import param_shift -login_key = "ZtQYpi6GVW24lrSOpauj16mRCAFrWN/3Et4xJjhn7dg=" +login_key = "KEY" class CQLibDevice(Device): short_name = 'cqlib.device' pennylane_requires = '0.42.1' -- Gitee From 63534f1c87013a3c47384f1700f6e05c61eb5f33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=9A=E5=AF=85?= <13017899+hefei-wu-yanzu@user.noreply.gitee.com> Date: Wed, 20 Aug 2025 08:54:21 +0000 Subject: [PATCH 05/19] update pyproject.toml. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 阚寅 <13017899+hefei-wu-yanzu@user.noreply.gitee.com> --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 467ec29..0ffe50a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,6 @@ [project] name = "cqlib-adapter" +version = "0.1.0" description = "Cqlib adapter" authors = [ { name = "Gao Jianjian", email = "jianjian001@outlook.com" } -- Gitee From ab115126b50c920d55b37d939715d398ffe4fd8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=9A=E5=AF=85?= <13017899+hefei-wu-yanzu@user.noreply.gitee.com> Date: Wed, 20 Aug 2025 10:00:06 +0000 Subject: [PATCH 06/19] update cqlib_adapter/pennylane_ext/device.py. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 阚寅 <13017899+hefei-wu-yanzu@user.noreply.gitee.com> --- cqlib_adapter/pennylane_ext/device.py | 87 ++------------------------- 1 file changed, 6 insertions(+), 81 deletions(-) diff --git a/cqlib_adapter/pennylane_ext/device.py b/cqlib_adapter/pennylane_ext/device.py index 50d567d..2654833 100644 --- a/cqlib_adapter/pennylane_ext/device.py +++ b/cqlib_adapter/pennylane_ext/device.py @@ -8,12 +8,12 @@ import json from cqlib.utils import qasm2 from os import path from cqlib.mapping import transpile_qcis -from cqlib.simulator import SimpleSimulator, StatevectorSimulator +from cqlib.simulator import StatevectorSimulator from typing import Union, List, Dict from pennylane.tape import QuantumScript -from pennylane.gradients import param_shift -login_key = "KEY" + + class CQLibDevice(Device): short_name = 'cqlib.device' pennylane_requires = '0.42.1' @@ -21,17 +21,13 @@ class CQLibDevice(Device): author = 'Ky' config_filepath = path.join(path.dirname(__file__), "cqlib_config.toml") - def __init__(self, wires, shots=None, cqlib_backend_name="default", **kwargs): + def __init__(self, wires, shots=None, cqlib_backend_name="default", login_key = None): super().__init__(wires=wires, shots=shots) self.num_wires = wires self.num_shots = shots self.machine_name = cqlib_backend_name - - self.cqlib_backend = TianYanPlatform(login_key=login_key, machine_name=cqlib_backend_name) - - - # self._gradient_transform = param_shift - # self._gradient_kwargs = {} + if cqlib_backend_name != "default": + self.cqlib_backend = TianYanPlatform(login_key=login_key, machine_name=cqlib_backend_name) def name(self): return 'CQLib Quantum Device' @classmethod @@ -53,10 +49,6 @@ class CQLibDevice(Device): "torch": "default.qubit.torch", "jax": "default.qubit.jax", }, - - # ✅ 新增梯度相关字段 - supports_derivatives=True, # 表明支持梯度 - grad_methods={"device": True} # 支持 diff_method="device" ) return capabilities @@ -234,73 +226,6 @@ class CQLibDevice(Device): def __repr__(self): return f"<{self.name()} device (wires={self.num_wires}, shots={self.shots})>" - - def gradient(self, tape, **kwargs): - """ - 输入: - tape: QNode 的量子操作序列 - 返回: - 对每个可微参数的梯度 - """ - grads = [] - # 遍历 tape 中的参数 - for op in tape.operations: - for i, param in enumerate(op.parameters): - # 这里用参数移位法举例 - shift = np.pi/2 - op.parameters[i] += shift - forward_plus = self.execute(tape) - op.parameters[i] -= 2*shift - forward_minus = self.execute(tape) - op.parameters[i] += shift # 复原 - - grad = (forward_plus - forward_minus) / 2.0 - grads.append(grad) - return np.array(grads) - - def compute_derivatives(self, circuits: QuantumScriptOrBatch): - if isinstance(circuits, qml.tape.QuantumScript): - circuits = [circuits] - - gradients = [] - for circuit in circuits: - params = circuit.get_parameters() - grad = np.zeros(len(params)) - - for i in range(len(params)): - # 创建参数移位后的电路 - shifted_plus = circuit.copy() - shifted_minus = circuit.copy() - - # 正确设置参数 - new_params_plus = params.copy() - new_params_minus = params.copy() - new_params_plus[i] += np.pi/2 - new_params_minus[i] -= np.pi/2 - - shifted_plus.set_parameters(new_params_plus) - shifted_minus.set_parameters(new_params_minus) - - # 执行移位电路 - res_plus = self.execute(shifted_plus)[0] # 确保获取标量结果 - res_minus = self.execute(shifted_minus)[0] - - # 标准参数移位公式 - grad[i] = (res_plus - res_minus) / 2 - - gradients.append(grad) - - return gradients[0] if len(gradients) == 1 else gradients - def supports_operation(self, op): - supported_ops = {"RX", "RY", "CNOT"} # 根据硬件调整 - return getattr(op, "name", None) in supported_ops - - def supports_derivatives(self, execution_config=None, circuit=None): - """声明设备支持导数计算""" - return False - - def __repr__(self): - return f"<{self.name()} device (wires={self.num_wires}, shots={self.shots})>" def extract_probability(json_data: List[Dict[str, Union[Dict[str, float], list]]], num_wires: int) -> Dict: -- Gitee From b7daa8be905dfb105f3ef7bfd8898cda6f525343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=9A=E5=AF=85?= <13017899+hefei-wu-yanzu@user.noreply.gitee.com> Date: Wed, 20 Aug 2025 10:02:22 +0000 Subject: [PATCH 07/19] update cqlib_adapter/pennylane_ext/device.py. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 阚寅 <13017899+hefei-wu-yanzu@user.noreply.gitee.com> --- cqlib_adapter/pennylane_ext/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cqlib_adapter/pennylane_ext/device.py b/cqlib_adapter/pennylane_ext/device.py index 2654833..39749fd 100644 --- a/cqlib_adapter/pennylane_ext/device.py +++ b/cqlib_adapter/pennylane_ext/device.py @@ -16,7 +16,7 @@ from pennylane.tape import QuantumScript class CQLibDevice(Device): short_name = 'cqlib.device' - pennylane_requires = '0.42.1' + pennylane_requires = '>=0.42.1' version = '0.1.0' author = 'Ky' config_filepath = path.join(path.dirname(__file__), "cqlib_config.toml") -- Gitee From d55f173b85bf3048a9b764936c0e32a0537e22ba Mon Sep 17 00:00:00 2001 From: Kan Yin <54517467+KanDaoZhang@users.noreply.github.com> Date: Thu, 21 Aug 2025 09:42:49 +0800 Subject: [PATCH 08/19] =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_pennylane/test.py | 62 ++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tests/test_pennylane/test.py diff --git a/tests/test_pennylane/test.py b/tests/test_pennylane/test.py new file mode 100644 index 0000000..03150ab --- /dev/null +++ b/tests/test_pennylane/test.py @@ -0,0 +1,62 @@ +import pennylane as qml +from pennylane import numpy as np + + +TOKEN = "your_token" +# 如果只使用本地模拟器,可以将login_key设置为None +dev = qml.device('cqlib.device', wires=2, shots=500, cqlib_backend_name="default",login_key = TOKEN) + + +@qml.qnode(dev, diff_method="parameter-shift") +def circuit(params): + qml.RX(params[0], wires=0) + qml.RY(params[1], wires=1) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(0)) +params = np.array([0.5, 0.8], requires_grad=True) +# 初始化设备 +def test(cqlib_backend_name): + dev = qml.device('cqlib.device', wires=2, shots=500, cqlib_backend_name=cqlib_backend_name) + + # 定义一个简单的量子电路 + # @qml.qnode(dev) + # def circuit(params): + # qml.RX(params[0], wires=0) + # qml.RY(params[1], wires=1) + # qml.CNOT(wires=[0, 1]) + # return qml.expval(qml.PauliZ(0)) + + + @qml.qnode(dev) + def circuit_probs(params): + qml.RX(params[0], wires=0) + qml.RY(params[1], wires=1) + qml.CNOT(wires=[0, 1]) + return qml.probs(wires=[0, 1]) # 返回4个概率值 + + @qml.qnode(dev) + def circuit_expval(params): + qml.RX(params[0], wires=0) + qml.RY(params[1], wires=1) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(0)) # 返回期望值 + + params = np.array([0.5, 0.8], requires_grad=True) + probs = circuit_probs(params) + expval = circuit_expval(params) + print("概率分布:", probs) + print("期望值:", expval) + +# test("tianyan504") +# test("tianyan_sw") +# test("default") +# 进行简单的优化(模拟量子机器学习) +opt = qml.GradientDescentOptimizer(stepsize=0.1) +steps = 10 +for i in range(steps): + params = opt.step(circuit, params) + + print(f"步骤 {i + 1}: 参数 = {params}, 期望值 = {circuit(params)}") + +# print("最终参数:", params) +# print("最终期望值:", circuit(params)) \ No newline at end of file -- Gitee From 22aca59fd4456d0da7da0e3014854cb1ef1a3e6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=9A=E5=AF=85?= <13017899+hefei-wu-yanzu@user.noreply.gitee.com> Date: Fri, 22 Aug 2025 11:34:12 +0800 Subject: [PATCH 09/19] =?UTF-8?q?=E6=8F=8F=E8=BF=B0=E6=82=A8=E6=89=80?= =?UTF-8?q?=E5=81=9A=E7=9A=84=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cqlib_adapter/pennylane_ext/.keep | 0 cqlib_adapter/pennylane_ext/device.py | 240 +++++++++++++++++--------- tests/test_pennylane/test.py | 11 +- 3 files changed, 156 insertions(+), 95 deletions(-) delete mode 100644 cqlib_adapter/pennylane_ext/.keep diff --git a/cqlib_adapter/pennylane_ext/.keep b/cqlib_adapter/pennylane_ext/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/cqlib_adapter/pennylane_ext/device.py b/cqlib_adapter/pennylane_ext/device.py index 39749fd..980b4d6 100644 --- a/cqlib_adapter/pennylane_ext/device.py +++ b/cqlib_adapter/pennylane_ext/device.py @@ -1,3 +1,4 @@ +# 导入必要的库和模块 import pennylane as qml from pennylane.devices import Device, DefaultExecutionConfig from pennylane.tape import QuantumScriptOrBatch @@ -13,36 +14,59 @@ from typing import Union, List, Dict from pennylane.tape import QuantumScript - class CQLibDevice(Device): - short_name = 'cqlib.device' - pennylane_requires = '>=0.42.1' - version = '0.1.0' - author = 'Ky' - config_filepath = path.join(path.dirname(__file__), "cqlib_config.toml") + """自定义量子设备类,基于CQLib后端实现PennyLane设备接口""" + + # 设备元数据 + short_name = 'cqlib.device' + pennylane_requires = '>=0.42.1' # 依赖的PennyLane版本 + version = '0.1.0' # 设备版本号 + author = '' + config_filepath = path.join(path.dirname(__file__), "cqlib_config.toml") # 配置文件路径 - def __init__(self, wires, shots=None, cqlib_backend_name="default", login_key = None): + def __init__(self, wires, shots=None, cqlib_backend_name="default", login_key=None): + """ + 初始化CQLib设备 + + Args: + wires: 量子比特数量 + shots: 测量次数,None表示使用解析计算 + cqlib_backend_name: CQLib后端名称,默认为"default" + login_key: 天衍平台登录密钥(如果需要) + """ super().__init__(wires=wires, shots=shots) - self.num_wires = wires - self.num_shots = shots - self.machine_name = cqlib_backend_name + self.num_wires = wires # 量子比特数量 + self.num_shots = shots # 测量次数 + self.machine_name = cqlib_backend_name # 后端名称 + + # 如果不是默认后端,初始化天衍云平台连接 if cqlib_backend_name != "default": self.cqlib_backend = TianYanPlatform(login_key=login_key, machine_name=cqlib_backend_name) + def name(self): + """返回设备名称""" return 'CQLib Quantum Device' + @classmethod def capabilities(cls): + """ + 返回设备能力配置 + + Returns: + dict: 包含设备支持的功能和特性的字典 + """ # 获取父类能力字典 - capabilities = super().capabilities().copy() # 必须创建副本以避免修改父类属性 + capabilities = super().capabilities().copy() # 更新设备特定能力 capabilities.update( - model="qubit", # 或 "cv" (连续变量) - supports_inverse_operations=True, - supports_analytic_computation=True, - supports_finite_shots=True, + model="qubit", # 量子比特模型(非连续变量) + supports_inverse_operations=False, # 是否支持逆操作 + supports_analytic_computation=False, # 是否支持解析计算 + supports_finite_shots=True, # 支持有限次测量 - returns_state=False, + returns_state=False, # 是否返回量子态 + # 支持的自动微分框架对应的设备 passthru_devices={ "autograd": "default.qubit.autograd", "tf": "default.qubit.tf", @@ -52,172 +76,214 @@ class CQLibDevice(Device): ) return capabilities + def execute(self, circuits: QuantumScriptOrBatch, execution_config=None): - """Execute quantum circuits on the target backend. + """ + 在目标后端执行量子线路 Args: - circuits: Quantum circuit(s) to execute - execution_config: Execution configuration + circuits: 要执行的量子线路(单个或多个) + execution_config: 执行配置 Returns: - List of results corresponding to each circuit's measurements + list: 每个线路的测量结果列表 """ - # Ensure circuits is a list even if single circuit is provided + # 确保circuits是列表(即使只提供单个线路) circuits = [circuits] if isinstance(circuits, qml.tape.QuantumScript) else circuits - results = [] + results = [] # 存储所有线路的结果 for circuit in circuits: - # Convert circuit to QCIS format - qasm_str = circuit.to_openqasm() - cqlib_cir = qasm2.loads(qasm_str) - cqlib_qcis = cqlib_cir.qcis - print(qasm_str) + # 将线路转换为QCIS格式 + qasm_str = circuit.to_openqasm() # 转换为OpenQASM字符串 + cqlib_cir = qasm2.loads(qasm_str) # 加载为CQLib线路对象 + cqlib_qcis = cqlib_cir.qcis # 获取QCIS格式的线路 # 根据后端类型选择执行方式 - # 此处约定将每一种后端的计算结果都调整为一个dict或者dict的list,方便统一处理 + # 约定将每种后端的计算结果都调整为dict或dict的list,方便统一处理 if self._is_tianyan_hardware(): + # 天衍硬件后端:需要编译和提交实验 compiled_circuit = transpile_qcis(cqlib_qcis, self.cqlib_backend) query_id = self.cqlib_backend.submit_experiment(compiled_circuit[0].qcis, num_shots=self.num_shots) raw_result = self.cqlib_backend.query_experiment(query_id, readout_calibration=True) res = extract_probability(raw_result, num_wires=self.num_wires) - # print(res) - + elif self._is_tianyan_simulator(): + # 天衍仿真机后端:直接提交实验 query_id = self.cqlib_backend.submit_experiment(cqlib_qcis, num_shots=self.num_shots) raw_result = self.cqlib_backend.query_experiment(query_id) res = extract_probability(raw_result, num_wires=self.num_wires) - # print(res) + else: - # caution: 本地模拟器只接受线路对象,不是qcis串 + # 本地仿真机:直接执行线路 + # 注意:本地仿真机只接受传入线路对象 res = self._execute_on_simulator(cqlib_cir) - # print(res) - # Process measurements + # 处理测量结果 circuit_results = self._process_measurements(circuit, res) results.append(circuit_results) return results def _is_tianyan_hardware(self): - """Check if the current backend is Tianyan hardware.""" + """检查当前后端是否为天衍硬件""" return self.machine_name in {"tianyan24", "tianyan504", "tianyan176-2", "tianyan176"} def _is_tianyan_simulator(self): - """Check if the current backend is Tianyan simulator.""" + """检查当前后端是否为天衍仿真机""" return self.machine_name in {"tianyan_sw", "tianyan_s", "tianyan_tn", "tianyan_tnn", "tianyan_sa", "tianyan_swn"} def _execute_on_simulator(self, circuit): - """Execute circuit on local simulator.""" - - # simulator = SimpleSimulator(circuit, device="cpu") - simulator = StatevectorSimulator(circuit) - return simulator.sample() + """在本地仿真机上执行线路""" + simulator = StatevectorSimulator(circuit) # 创建态矢量仿真机 + return simulator.sample() # 返回采样结果 def _process_measurements(self, circuit: QuantumScript, raw_result: Union[dict, List[dict]]) -> Union[float, np.ndarray, List[Union[float, np.ndarray]]]: """ - Process measurements based on circuit's measurement operations. - + 根据线路的测量操作处理测量结果 + Args: - circuit: PennyLane quantum circuit. - raw_result: Raw result from the backend (simulator or hardware). - + circuit: PennyLane量子线路 + raw_result: 从后端获取的原始结果(仿真机或硬件) + Returns: - Measurement results (probabilities or expectations). + Union[float, np.ndarray, List]: 测量结果(概率或期望值) """ results = [] for meas in circuit.measurements: if isinstance(meas, qml.measurements.ExpectationMP): + # 处理期望值测量 results.append(self._process_expectation(meas, raw_result)) elif isinstance(meas, qml.measurements.ProbabilityMP): + # 处理概率测量 results.append(self._process_probability(meas, raw_result)) else: raise NotImplementedError(f"Measurement type {type(meas).__name__} is not supported") + + # 如果只有一个测量结果,直接返回它而不是列表 return results[0] if len(results) == 1 else results - def _process_expectation(self, meas, raw_result) -> float: - """Process expectation value measurement.""" - # 目前只支持 PauliZ 的期望 + """处理期望值测量""" + # 目前只支持PauliZ的期望 if meas.obs.name != "PauliZ": raise NotImplementedError(f"Expectation for {meas.obs.name} is not supported") - # 使用 local 或 hardware 解析 + # 根据结果类型选择处理方法 if isinstance(raw_result, list): return self.process_results(raw_result) elif isinstance(raw_result, dict): - local_exp = self.process_results_local(raw_result) - return local_exp[meas.wires[0]] + local_exp = self.process_results_local(raw_result) + return local_exp[meas.wires[0]] else: raise ValueError(f"Unsupported raw_result type: {type(raw_result)}") - def _process_probability(self, meas, raw_result) -> np.ndarray: - """Process probability measurement.""" - num_wires = len(meas.wires) - probs = np.zeros(2**num_wires) + """处理概率测量""" + num_wires = len(meas.wires) + probs = np.zeros(2**num_wires) - if isinstance(raw_result, dict): # 本地 simulator + if isinstance(raw_result, dict): total_shots = sum(raw_result.values()) for bitstring, count in raw_result.items(): - index = int(bitstring[::-1], 2) - probs[index] = count / total_shots - elif isinstance(raw_result, list): # 硬件或云 simulator + # 将比特字符串转换为概率数组索引 + index = int(bitstring[::-1], 2) + probs[index] = count / total_shots + + elif isinstance(raw_result, list): try: + # 解析JSON格式的概率分布 prob_dict = json.loads(raw_result[0]["probability"]) for bitstring, prob in prob_dict.items(): - index = int(bitstring[::-1], 2) - probs[index] = prob + index = int(bitstring[::-1], 2) + probs[index] = prob except (json.JSONDecodeError, KeyError, TypeError) as e: raise ValueError(f"Invalid raw_result format: {e}") else: raise ValueError(f"Unsupported raw_result type: {type(raw_result)}") - # 检查概率和 + # 检查概率和是否为1(允许小的数值误差) if not np.isclose(np.sum(probs), 1.0, rtol=1e-5): raise ValueError(f"Probabilities do not sum to 1: {np.sum(probs)}") return probs + def process_results(self, raw_result): - # 解析概率分布(例如:{"00":0.788, "11":0.044, ...}) + """ + 处理硬件或云仿真机的期望值结果 + + Args: + raw_result: 原始结果数据 + + Returns: + float: PauliZ期望值 + """ prob_dict = json.loads(raw_result[0]["probability"]) expectation = 0.0 for state, prob in prob_dict.items(): - # 计算第一个量子比特的 Z 期望值 - if state[0] == "0": # |0⟩ 对应 Z 的本征值 +1 + if state[0] == "0": # |0⟩态对应Z的本征值+1 expectation += prob - else: # |1⟩ 对应 Z 的本征值 -1 + else: # |1⟩态对应Z的本征值-1 expectation -= prob return expectation + def process_results_local(self, raw_result): - total_shots = sum(raw_result.values()) - - # 初始化每个量子比特的计数 - z_expectations = {} - num_qubits = len(next(iter(raw_result.keys()))) # 获取比特串长度(这里是2个量子比特) + """ + 处理本地仿真机的期望值结果 + + Args: + raw_result: 原始结果数据 + + Returns: + dict: 每个量子比特的PauliZ期望值 + """ + total_shots = sum(raw_result.values()) # 总测量次数 + num_qubits = len(next(iter(raw_result.keys()))) + z_expectations = {} for qubit in range(num_qubits): - n0, n1 = 0, 0 + n0, n1 = 0, 0 for bitstring, count in raw_result.items(): - # 检查当前量子比特的值(注意比特串顺序可能是小端序) - bit = int(bitstring[-qubit - 1]) # 假设右侧是qubit0,左侧是qubit1 + + bit = int(bitstring[-qubit - 1]) if bit == 0: n0 += count else: n1 += count + + # 计算Z期望值:(n0 - n1) / total_shots z_expectations[qubit] = (n0 - n1) / total_shots return z_expectations def preprocess_transforms(self, execution_config=None): + """ + 定义预处理转换流程 + + Returns: + TransformProgram: 预处理转换程序 + """ program = qml.transforms.core.TransformProgram() + # 添加设备线数验证 program.add_transform(qml.devices.preprocess.validate_device_wires, wires=self.wires, name=self.short_name) + # 添加测量验证 program.add_transform(qml.devices.preprocess.validate_measurements, name=self.short_name) + # 添加操作分解(使用supports_operation作为停止条件) program.add_transform(qml.devices.preprocess.decompose, stopping_condition=self.supports_operation, name=self.short_name) return program def supports_operation(self, op): + """ + 检查是否支持特定量子操作 + + Args: + op: 量子操作 + + Returns: + bool: 如果支持该操作返回True,否则返回False + """ + # 支持的量子操作集合 supported_ops = { "PauliX", "PauliY", "PauliZ", "Hadamard", "S", "T", "RX", "RY", "RZ", "CNOT", "CZ" @@ -225,23 +291,26 @@ class CQLibDevice(Device): return getattr(op, "name", None) in supported_ops def __repr__(self): + """返回设备的字符串表示""" return f"<{self.name()} device (wires={self.num_wires}, shots={self.shots})>" def extract_probability(json_data: List[Dict[str, Union[Dict[str, float], list]]], num_wires: int) -> Dict: """ - Extract probability distribution from JSON data. - + 从JSON数据中提取概率分布 + Args: - json_data: JSON data containing measurement results, expected as a list with a dictionary - containing a 'probability' field. - num_wires: Number of qubits (wires) in the circuit. - + json_data: 包含测量结果的JSON数据,期望为包含'dictionary'的列表, + 其中包含'probability'字段 + num_wires: 线路中的量子比特数(线数) + Returns: - Dict: Probability distribution for each qubit. + Dict: 每个量子比特的概率分布 + Raises: - ValueError: If JSON data is invalid or probability field is missing/invalid. + ValueError: 如果JSON数据无效或缺少概率字段 """ + # 验证输入数据格式 if not isinstance(json_data, list) or not json_data: raise ValueError("json_data must be a non-empty list") if not isinstance(json_data[0], dict): @@ -250,9 +319,10 @@ def extract_probability(json_data: List[Dict[str, Union[Dict[str, float], list]] raise ValueError("probability field missing in json_data[0]") try: + # 提取并解析概率字段 prob_dict = json_data[0]["probability"] if isinstance(prob_dict, str): - prob_dict = json.loads(prob_dict) + prob_dict = json.loads(prob_dict) # 如果概率字段是字符串,解析为字典 if not isinstance(prob_dict, dict): raise ValueError("probability field must be a dictionary") except (KeyError, TypeError) as e: diff --git a/tests/test_pennylane/test.py b/tests/test_pennylane/test.py index 03150ab..4f591b0 100644 --- a/tests/test_pennylane/test.py +++ b/tests/test_pennylane/test.py @@ -17,16 +17,7 @@ params = np.array([0.5, 0.8], requires_grad=True) # 初始化设备 def test(cqlib_backend_name): dev = qml.device('cqlib.device', wires=2, shots=500, cqlib_backend_name=cqlib_backend_name) - # 定义一个简单的量子电路 - # @qml.qnode(dev) - # def circuit(params): - # qml.RX(params[0], wires=0) - # qml.RY(params[1], wires=1) - # qml.CNOT(wires=[0, 1]) - # return qml.expval(qml.PauliZ(0)) - - @qml.qnode(dev) def circuit_probs(params): qml.RX(params[0], wires=0) @@ -41,7 +32,7 @@ def test(cqlib_backend_name): qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0)) # 返回期望值 - params = np.array([0.5, 0.8], requires_grad=True) + params = np.array([0.5, 0.8]) probs = circuit_probs(params) expval = circuit_expval(params) print("概率分布:", probs) -- Gitee From 5aa7d35a2d0992477a411ba3f139b331fb806047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=9A=E5=AF=85?= <13017899+hefei-wu-yanzu@user.noreply.gitee.com> Date: Fri, 22 Aug 2025 16:21:57 +0800 Subject: [PATCH 10/19] =?UTF-8?q?=E6=B7=BB=E5=8A=A0Pennylane=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E4=BB=A3=E7=A0=81=E5=92=8C=E6=B5=8B=E8=AF=95=E7=BB=93?= =?UTF-8?q?=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cql_adp/Scripts/Activate.ps1 | 247 ++++++++++++++++++++++++++ cql_adp/Scripts/activate | 69 +++++++ cql_adp/Scripts/activate.bat | 34 ++++ cql_adp/Scripts/deactivate.bat | 22 +++ cql_adp/Scripts/pip.exe | Bin 0 -> 108416 bytes cql_adp/Scripts/pip3.10.exe | Bin 0 -> 108416 bytes cql_adp/Scripts/pip3.exe | Bin 0 -> 108416 bytes cql_adp/Scripts/python.exe | Bin 0 -> 259584 bytes cql_adp/Scripts/pythonw.exe | Bin 0 -> 247808 bytes cql_adp/pyvenv.cfg | 3 + cqlib_adapter/pennylane_ext/device.py | 17 +- tests/test_pennylane/test.py | 2 +- 12 files changed, 391 insertions(+), 3 deletions(-) create mode 100644 cql_adp/Scripts/Activate.ps1 create mode 100644 cql_adp/Scripts/activate create mode 100644 cql_adp/Scripts/activate.bat create mode 100644 cql_adp/Scripts/deactivate.bat create mode 100644 cql_adp/Scripts/pip.exe create mode 100644 cql_adp/Scripts/pip3.10.exe create mode 100644 cql_adp/Scripts/pip3.exe create mode 100644 cql_adp/Scripts/python.exe create mode 100644 cql_adp/Scripts/pythonw.exe create mode 100644 cql_adp/pyvenv.cfg diff --git a/cql_adp/Scripts/Activate.ps1 b/cql_adp/Scripts/Activate.ps1 new file mode 100644 index 0000000..eeea358 --- /dev/null +++ b/cql_adp/Scripts/Activate.ps1 @@ -0,0 +1,247 @@ +<# +.Synopsis +Activate a Python virtual environment for the current PowerShell session. + +.Description +Pushes the python executable for a virtual environment to the front of the +$Env:PATH environment variable and sets the prompt to signify that you are +in a Python virtual environment. Makes use of the command line switches as +well as the `pyvenv.cfg` file values present in the virtual environment. + +.Parameter VenvDir +Path to the directory that contains the virtual environment to activate. The +default value for this is the parent of the directory that the Activate.ps1 +script is located within. + +.Parameter Prompt +The prompt prefix to display when this virtual environment is activated. By +default, this prompt is the name of the virtual environment folder (VenvDir) +surrounded by parentheses and followed by a single space (ie. '(.venv) '). + +.Example +Activate.ps1 +Activates the Python virtual environment that contains the Activate.ps1 script. + +.Example +Activate.ps1 -Verbose +Activates the Python virtual environment that contains the Activate.ps1 script, +and shows extra information about the activation as it executes. + +.Example +Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv +Activates the Python virtual environment located in the specified location. + +.Example +Activate.ps1 -Prompt "MyPython" +Activates the Python virtual environment that contains the Activate.ps1 script, +and prefixes the current prompt with the specified string (surrounded in +parentheses) while the virtual environment is active. + +.Notes +On Windows, it may be required to enable this Activate.ps1 script by setting the +execution policy for the user. You can do this by issuing the following PowerShell +command: + +PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser + +For more information on Execution Policies: +https://go.microsoft.com/fwlink/?LinkID=135170 + +#> +Param( + [Parameter(Mandatory = $false)] + [String] + $VenvDir, + [Parameter(Mandatory = $false)] + [String] + $Prompt +) + +<# Function declarations --------------------------------------------------- #> + +<# +.Synopsis +Remove all shell session elements added by the Activate script, including the +addition of the virtual environment's Python executable from the beginning of +the PATH variable. + +.Parameter NonDestructive +If present, do not remove this function from the global namespace for the +session. + +#> +function global:deactivate ([switch]$NonDestructive) { + # Revert to original values + + # The prior prompt: + if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { + Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt + Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT + } + + # The prior PYTHONHOME: + if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { + Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME + Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME + } + + # The prior PATH: + if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { + Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH + Remove-Item -Path Env:_OLD_VIRTUAL_PATH + } + + # Just remove the VIRTUAL_ENV altogether: + if (Test-Path -Path Env:VIRTUAL_ENV) { + Remove-Item -Path env:VIRTUAL_ENV + } + + # Just remove VIRTUAL_ENV_PROMPT altogether. + if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) { + Remove-Item -Path env:VIRTUAL_ENV_PROMPT + } + + # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: + if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { + Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force + } + + # Leave deactivate function in the global namespace if requested: + if (-not $NonDestructive) { + Remove-Item -Path function:deactivate + } +} + +<# +.Description +Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the +given folder, and returns them in a map. + +For each line in the pyvenv.cfg file, if that line can be parsed into exactly +two strings separated by `=` (with any amount of whitespace surrounding the =) +then it is considered a `key = value` line. The left hand string is the key, +the right hand is the value. + +If the value starts with a `'` or a `"` then the first and last character is +stripped from the value before being captured. + +.Parameter ConfigDir +Path to the directory that contains the `pyvenv.cfg` file. +#> +function Get-PyVenvConfig( + [String] + $ConfigDir +) { + Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" + + # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). + $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue + + # An empty map will be returned if no config file is found. + $pyvenvConfig = @{ } + + if ($pyvenvConfigPath) { + + Write-Verbose "File exists, parse `key = value` lines" + $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath + + $pyvenvConfigContent | ForEach-Object { + $keyval = $PSItem -split "\s*=\s*", 2 + if ($keyval[0] -and $keyval[1]) { + $val = $keyval[1] + + # Remove extraneous quotations around a string value. + if ("'""".Contains($val.Substring(0, 1))) { + $val = $val.Substring(1, $val.Length - 2) + } + + $pyvenvConfig[$keyval[0]] = $val + Write-Verbose "Adding Key: '$($keyval[0])'='$val'" + } + } + } + return $pyvenvConfig +} + + +<# Begin Activate script --------------------------------------------------- #> + +# Determine the containing directory of this script +$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +$VenvExecDir = Get-Item -Path $VenvExecPath + +Write-Verbose "Activation script is located in path: '$VenvExecPath'" +Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" +Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" + +# Set values required in priority: CmdLine, ConfigFile, Default +# First, get the location of the virtual environment, it might not be +# VenvExecDir if specified on the command line. +if ($VenvDir) { + Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" +} +else { + Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." + $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") + Write-Verbose "VenvDir=$VenvDir" +} + +# Next, read the `pyvenv.cfg` file to determine any required value such +# as `prompt`. +$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir + +# Next, set the prompt from the command line, or the config file, or +# just use the name of the virtual environment folder. +if ($Prompt) { + Write-Verbose "Prompt specified as argument, using '$Prompt'" +} +else { + Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" + if ($pyvenvCfg -and $pyvenvCfg['prompt']) { + Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" + $Prompt = $pyvenvCfg['prompt']; + } + else { + Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)" + Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" + $Prompt = Split-Path -Path $venvDir -Leaf + } +} + +Write-Verbose "Prompt = '$Prompt'" +Write-Verbose "VenvDir='$VenvDir'" + +# Deactivate any currently active virtual environment, but leave the +# deactivate function in place. +deactivate -nondestructive + +# Now set the environment variable VIRTUAL_ENV, used by many tools to determine +# that there is an activated venv. +$env:VIRTUAL_ENV = $VenvDir + +if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { + + Write-Verbose "Setting prompt to '$Prompt'" + + # Set the prompt to include the env name + # Make sure _OLD_VIRTUAL_PROMPT is global + function global:_OLD_VIRTUAL_PROMPT { "" } + Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT + New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt + + function global:prompt { + Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " + _OLD_VIRTUAL_PROMPT + } + $env:VIRTUAL_ENV_PROMPT = $Prompt +} + +# Clear PYTHONHOME +if (Test-Path -Path Env:PYTHONHOME) { + Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME + Remove-Item -Path Env:PYTHONHOME +} + +# Add the venv to the PATH +Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH +$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" diff --git a/cql_adp/Scripts/activate b/cql_adp/Scripts/activate new file mode 100644 index 0000000..3fdf6ca --- /dev/null +++ b/cql_adp/Scripts/activate @@ -0,0 +1,69 @@ +# This file must be used with "source bin/activate" *from bash* +# you cannot run it directly + +deactivate () { + # reset old environment variables + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + + # This should detect bash and zsh, which have a hash command that must + # be called to get it to forget past commands. Without forgetting + # past commands the $PATH changes we made may not be respected + if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null + fi + + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + # Self destruct! + unset -f deactivate + fi +} + +# unset irrelevant variables +deactivate nondestructive + +VIRTUAL_ENV='C:\Users\ROG\lzw\cqlib-adapter\cql_adp' +export VIRTUAL_ENV + +_OLD_VIRTUAL_PATH="$PATH" +PATH="$VIRTUAL_ENV/"Scripts":$PATH" +export PATH + +# unset PYTHONHOME if set +# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) +# could use `if (set -u; : $PYTHONHOME) ;` in bash +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" + unset PYTHONHOME +fi + +if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then + _OLD_VIRTUAL_PS1="${PS1:-}" + PS1='(cql_adp) '"${PS1:-}" + export PS1 + VIRTUAL_ENV_PROMPT='(cql_adp) ' + export VIRTUAL_ENV_PROMPT +fi + +# This should detect bash and zsh, which have a hash command that must +# be called to get it to forget past commands. Without forgetting +# past commands the $PATH changes we made may not be respected +if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null +fi diff --git a/cql_adp/Scripts/activate.bat b/cql_adp/Scripts/activate.bat new file mode 100644 index 0000000..1c3f73c --- /dev/null +++ b/cql_adp/Scripts/activate.bat @@ -0,0 +1,34 @@ +@echo off + +rem This file is UTF-8 encoded, so we need to update the current code page while executing it +for /f "tokens=2 delims=:." %%a in ('"%SystemRoot%\System32\chcp.com"') do ( + set _OLD_CODEPAGE=%%a +) +if defined _OLD_CODEPAGE ( + "%SystemRoot%\System32\chcp.com" 65001 > nul +) + +set "VIRTUAL_ENV=C:\Users\ROG\lzw\cqlib-adapter\cql_adp" + +if not defined PROMPT set PROMPT=$P$G + +if defined _OLD_VIRTUAL_PROMPT set PROMPT=%_OLD_VIRTUAL_PROMPT% +if defined _OLD_VIRTUAL_PYTHONHOME set PYTHONHOME=%_OLD_VIRTUAL_PYTHONHOME% + +set _OLD_VIRTUAL_PROMPT=%PROMPT% +set PROMPT=(cql_adp) %PROMPT% + +if defined PYTHONHOME set _OLD_VIRTUAL_PYTHONHOME=%PYTHONHOME% +set PYTHONHOME= + +if defined _OLD_VIRTUAL_PATH set PATH=%_OLD_VIRTUAL_PATH% +if not defined _OLD_VIRTUAL_PATH set _OLD_VIRTUAL_PATH=%PATH% + +set "PATH=%VIRTUAL_ENV%\Scripts;%PATH%" +set "VIRTUAL_ENV_PROMPT=(cql_adp) " + +:END +if defined _OLD_CODEPAGE ( + "%SystemRoot%\System32\chcp.com" %_OLD_CODEPAGE% > nul + set _OLD_CODEPAGE= +) diff --git a/cql_adp/Scripts/deactivate.bat b/cql_adp/Scripts/deactivate.bat new file mode 100644 index 0000000..62a39a7 --- /dev/null +++ b/cql_adp/Scripts/deactivate.bat @@ -0,0 +1,22 @@ +@echo off + +if defined _OLD_VIRTUAL_PROMPT ( + set "PROMPT=%_OLD_VIRTUAL_PROMPT%" +) +set _OLD_VIRTUAL_PROMPT= + +if defined _OLD_VIRTUAL_PYTHONHOME ( + set "PYTHONHOME=%_OLD_VIRTUAL_PYTHONHOME%" + set _OLD_VIRTUAL_PYTHONHOME= +) + +if defined _OLD_VIRTUAL_PATH ( + set "PATH=%_OLD_VIRTUAL_PATH%" +) + +set _OLD_VIRTUAL_PATH= + +set VIRTUAL_ENV= +set VIRTUAL_ENV_PROMPT= + +:END diff --git a/cql_adp/Scripts/pip.exe b/cql_adp/Scripts/pip.exe new file mode 100644 index 0000000000000000000000000000000000000000..1e8f8aa05097572e5cf97b200a4283d04eb38622 GIT binary patch literal 108416 zcmeFadw5jU)%ZWjWXKQ_P7p@IO-Bic#!G0tBo5RJ%;*`JC{}2xf}+8Qib}(bU_}i* zNt@v~ed)#4zP;$%+PC)dzP-K@u*HN(5-vi(8(ykWyqs}B0W}HN^ZTrQW|Da6`@GNh z?;nrOIeVXdS$plZ*IsMwwRUQ*Tjz4ST&_I+w{4fJg{S
9ahiv48usxvYph53A*~8(9C(zhxUuAG_s-p91ME#!0Q$JSe%fv0pf`Iy`k-vUY&tiPqL?X zvbdHFYS-%QRTNw0a;_E}o fZE#A@+KUZ! $4dp*1|c4o(ssj&>wkjNm~aX$iNMcV14@ZI|{H zteO#9yn&@U{r+j|$KTficN6^epS51~xY&fSu_`(9-m4Oc$sEe1%lMrkgUjW+tc!5e zgK{8^X`#jX1dbAKLcU~WI1ZN@hgR(%0-TSU^Zzg(+AFW7aED6TPGE$v?$2xWANhN3 zW^=8_`jB8w;_b6g-wYRiU%+k67$s$3wB$Xs=d4%s)FPu#V6f=L>+hd{RBmFN6nK~Q zA^ONfNwq$8>`Yr+CA|pKr0h>E5yX|AZ((`Y_fSPl*yW&O<`6hpr$o84=fePl5_C zaAEblI|_9p=={%tjKW&}Qy)B05hJb3$n&TS>r9<>y=?g_8$~(U+kv0F5JIzmL=C|Y zZ)J4f@p-JT{x2itfeVp|Ey%yJbBS+bz>^`fePLGA;jI0~kn)bwvfi#>U*yiT&fXvT z4rhDNs-1*Z?WeU??I8oHfTyh&-;zr7G(5#-l0>GH$oZj|R=mf_>Gl0sTV>q8Vl3wn zdnv2JW@#f$u?hH`amgUb2{IfW&n>$;Q@%~zNn~pY1t+^N;^&?Q*%BichZ7V)-sAVM z` bpKsGH=pT&i!vuH0x=%)GL8)31qNbEr*FT7eaVPc5%> zpSU6JKHQejp@j%9+xp|%wukSC2Lw+t^xt&FptzLtz_Eqqf~G!ooqABDH)4e{92UxX zMrX>|
0LWzQKOtB?ny+XZb^ =4+M+5=f4>c;9Ej z7tu5vdBuH+=f+s
r}mV#cafb!(7!3=m#mFD z_fnX*eH*epc{IzneS5Rx3ZQ|aZ|1dqqFdH!WBEMP_8uSFwjBftUrA^ogl_n> 2W*^$!WUD&UoL(n6bH?yJyA+6E+Oy7Cl-d z*t+q5LmxrcebPxks(H>oiW7E!(|QSy3YqK)OrF`)cT>_IS*7|zi958qAz7j8nwEO^ z` gOEPNKGP&=L73boh(8E8x%E b4b zzCsCqKgN_WpON=OB|MFS^ekbfl(0Vzx?I)bW1CPw`Y4B_T@^LCdx;WhZE~8UMWaMK z%03I?P-P1wuh|pXqop@jPoOUXq#rLL1;pD$P4W*WphWe+QQnqt>cn*J%P0?e1f 6Rp^+8hqunvz;&Sx6HQKa3hu^Pxm{_Jlp?Umh)V2_!_b2+z(u zcHOpiR_segNsE@x6z*V}0y7Ty&>(SrGz8JD2 8qn_-zOuCpD~#2Ct1kRYrW2tIXVZ7^q;c=qU}w6z5VCR3nEV6wu JZbuMb_Fh^uaF_0 jc?m?bbGyY)f%N3*m#X-rb81yl(n$b5OyH4h^jj z?;S>*F8#NTsyxwu`zS6w^xr;oqkHS{Nd33A(yL}}@yzu+)X;Z7uD%@>8n5(9>nI8; zWWMo*T3Et*8j8u8h>G9nHgK8^|8CpAX~WxX*gzIUq %yV^w8t3upxNUace9#R_-3US>Dy7DPR zH-)(8{clr sI!>Z{|SY-y7{zE zl2~;tT?%o}JK8P^aRFh4xZp84q4Rh&3#GaLe^7{f&ql_}6Dq_-9x>@zw!oTrkqU9s zhtdxIM+$LoB3j;6PL+6iQ;54 @oX!^J)DhX;)xaF))?PH z#uF>V{p6=%Li-~X;(l_LPRdb;YgD_+(m1RU_xThA%r=hJ8gZwykYvIM#QW-x#-WCr zrP-G&$h~>GS!8~hg4|gsU@Z$w;;*A1cN5oL-cM+6tUJ4cI~AQf kN}=GnIX}UEB2_!we3-nJ4x(IQ1C9W+|zKfKvd)o z7Kn=6egaXE+eaX(9OYh;s5dH BKPasgRLU>A}1PD exrbo}5QDqzeS^fby<-qp+v|cr^tiSI#wx0<1w^RUtBPDx8gX9O_ES7s zPhJ*YIbNG>tH}N4;mG?&EYL;JRWuG~upaoiA1cE%;+@V$9agp qUSN2^Q-L6iU zbJBmXKT0Ncwkei{jHg-6x4{Sz-MCj}&dMaM+RARaakH`NZGR*eT+%3S#Qtc2eh0 L$EcL`h|cCwTyo7meir45qW_ypeM~7y_JZ z!o4-OO5no44Mw7whm8*g&6N^i6-SLi^G4f7iHoo3`o5hAKhi0$yDG)Hg>ww&z#wln z-Dp=k3PBe!lIOQtcTY99OMLa;9Hcz!g{{VA#ti*NEh@III$w@_28a+m&$Pf=7e4g2 zzD+Ychgi++4r?lC-P)rnq~tnE_!fw4nd>A+^}7o%mwhrZr4v)|RLez(rprgOeS6d= zO?WMLNMwkL2;H`bZ@5+L_4@3MX8Xm I5|qfxsj}$AfKM?%H|l})Yttw(<>zSf^}rqQ^MA}coYYVK(Q7>GhiUuc z${xCjvd`w&MIU}pfKRhb;XMsMXINmy2 i-}^sUw=|1pn$$98FRi2rB9+R;a;6~fxl?~TJ;rMl$xRda5T${3Oy zd3HcHr@kNhl%wU)@8x_Z#hQLecs%;xTy`Fx5_w)|6e>%MdX`6KVIhaWG3nCOEP4Zc zd-0UnYP0|^pHUX&4^3ZECd?_G@4IEMKXdwgzJgU;s0@9;twqtX(*89#du}e1&FB~W zxU)H|w`<`#p%2|cPDbPn;=b1QYjjo68JYvb{1g7l*k-L~rzh%nWP=ro;f$?0Xia_J z-#8hPuJSide|3d)9@zT7Aa5Lph|XG?eXhijZ9Vz`F*e5TE`nKf_5H%GU%lG8>pso5 zueQ!u;?O`358-y-b@osD&mp!Lj`!Y@q{lS*-PTEUI?{PM<>mmKq%`PIU@{W)YAs0C z$Jc33XWO2BVmwWd&(H_br*8Cz`s7b|&mTILd*BOsAgwyT7?G^zK+Y3F`h3yTwO=aW zy#Hbv=Bh?;sNA5NJ!4v#r{NBKfF^> lzq zb$pN|ZU^7_g)Bk$*;kFFs=e0BnN0oS?Gody?T2{karT%c2aoy=41CE?U`<+E@hn+O zlbdqBhBeV6f+J~4DPrg4v@DAOSKpi)vqz59DP*iZW$o<_9b-s=3?DLb$R**>0pE6R zH?fFs=9V4@q$r^4b<9J@lzrO!?$l0sSMxj<5-Zb>m|=n?NT2|_D0xvAH7I0QtdNQO zJ(_tKvOPELAeGLPRQL_P-^s+ nJ=g@#ux^GYXpUE{ZwY%4mtMy` zdD-kT#=b{X9jwOZtT&0DvoK!6%*}kuA9^XrlfM`1d(0Ud7u{|%Ik|RN`|DOdG1q6r z1{16?I=LhQ`+2%b^zuJvamYnhSH{cONPldZdayI)YQEYRt-cIG5jmdDW*H}iH2NvA zXgf!$iFMgbydF8^ABJ4ZTij0d*P{@5ob|{8DVHQ npw}3AsEltK@!{1nR%n)CuKi>d2T@PY-k9ymfU~yL<&J9ht@~pg zsbzbf*zY^=DK|Z`I8|Q)#5N!|KM<`AqzObvgjXQiA^fxJ@?7pZ4#J-1X1&T-$G6IG zwWs&6zh2u%wWs3C<-V>x*>NWm*ksh9a3>h2b<*&_(vjDOHIGxx3MDOMLMqg4%m2u< zG{pMJd}m0u7SG_YTUf2_@uAq!aCI78P`uu`56<9JF*em 1t$8(4-nZr^QMU)K7yX6e z$OG3;c^em`w#}qp_VU1WdywMw^1$`3MHICA1J`3eavIco(vn!eGQfG;himmbayZOd zF+21mmL+5T*2{mE FA5+U{qO65&=u9G-(S%t(!U9u$k=_u#4Agc&UD^ zGa+fi!|qAl}W!VVpe1hJ4O)*{|ruTPSJsCDM^mN|0!6*EN}Rld XkX27H zll;60td$0~ShuqcVcI}V-QM<8lXBOjVC{hjqV&=bm-9K2MXRc$TmK#(B`Ad84-00! zBIKOUPopJ*M<^S2;j|FIWpNa_G4`${Qu5t?qnCl{`BrVg&HY3nNT5$=N+?!)N!!&q z&I0Wm_pbgc>~fOi&LgRM{h@bR*%w$JOb}s2b~jwpjC9GeUhL@tStLxM^@#0~9vNmk z!=bWPtm!2> Ct{ZaWhL_dg=sbxtI`?UY(s{cWdi36hm`YjV#_nu1YR2SRS^ z!Fzhk4da8dp7>^OPI}yycYu#0iI%6cHuUPGL#>Q(>QOw_6w1nva1Rr@{_#58*rS S#BR!2%5`H^JUW8LYM5t6CBi-t*er=)B!pCRzmQ8EXmAzy>l%Hj7up{f%TBR9RMK}mW|MUBQmIAG3NCQ{u z0~@L-=DVK_(` hN3LD;F!`p258yoJnVXF-f+t5AL#Gh)z(``7@hIuwzYQrmR zc)bmOXu~vFnD85H!#*~A?<`~gk?l`SGvA3e9BadwHoVY=SJ-fa4R5#MRvSKL!#8dC zfenw@aKLnv&M7v$(1wLJth8Z+4R5yLW*gpX!-s6R(}pkF@NFA**zi*u#-C}@_1f@s z8=hms`8NEz4XbUq!G@b`xY>sH+VBY*9 d$J8PZ0NV)*KN4UhBw&odp7*J z4Ii-K9vi-9!)bOs>dNKMGj=^bWWz&Fy*eIF05^{lrEW?MDl)L}pn=caZD7w}?$3;U z-6_4hNBVaqeXvZvWhs-7X+ 5lf9K$B+5tt0KOO70fdIn~UFN*aWqGWIRR0(`9SQqm;?N zf}WCJu0`s6O4%h}PJRrm b5 z_^R#UZ!!5O(IxNhvJl^;5x(=Gab-l<1-N(rmV7wrDq5MOr<93bz9l{>hr}cKmhh~6 z{AaIRd3J5ML6z`3-J8$PE68eo_##~X 9U$&QBAml&o8Rf zpQNiuOA)`st%y_N!&DM}wIVKwN6jr=rU;`J6a|7cB{=Y#TT^ah(4{O`Qycz*UZo|K zr4bejgXSy0s#5z}5VT=YK;n_`5=P-q;YZ;vNhnuTbWCiYICtOpgv6wNp5*=m1`bLY zJS27KNyCPZIC-RZ)aWr|$DJ}h?bOpIoIY{V z5Z6Eh{c5UB05M{E90pR#sM3f1{>0 z5WMQ@RjaT0=9;zFUZ>_%)#R)y4;0i?6_-lwuB0s$Q};Erf>Je!mQ 1^kQj $ap5>jf{=b z56da_3cf0J|1H;JTV!0~UQU|jxL5G^8rz@ro_O86O#I@n1ovX?E k%|D6Jgeb?QlKSvM87ZZSbtSekQhK$|E6Kmfdw^aorI%W)CB_Qvr%Ely zPU4d~bxJ1VQx}~kYC5eXZ5dN#%<-x;W`ttCYSgKGEhoN8zNO5PC$W*1AoP?H9Z#uB zokwXwW)6_@Nehb%nXU6Aqp9R;lCE88PfmSL3D qbeZN0_i)ooDPv6H7R z`c6@2h2wMb^VRC}YSQXG#op`G&|wOrhLiuVo}Tn9>9hZx^rnZ?tEP>bHgFYj)extw zIx3*r@jc1un_U!h@;@yc-&fE7<>Xw}N~=gWKpz$gIbYHuom%Wl&8hD*)QoU?z14RW zwJP;xMndV|ReH3LQL~gWQbw&(9fQ-39B9gOMvwL+xsn)Vd@y5 MC@_T%IE1|lKf kF|&gSBdxJJjbsld zzrtj*-;$G6{j ?eC%Xx7YqY$^PD&X#8`vLjSVtZ@HWyzm5ds&J_Ut+hTu@w7*;9jl0+WuC~8N z+23_; ()`k9?#x3GPbjc&-~JeK}L)U`k?&MDuWdjps?}#aHhxMYIGmf zCn`B6CnqOXe$&&5OFVir3YNsV)miE3iwoeNd%e1exeLn*`6;!kdKEu6K6rV-?FP8{ zC!hcMK>_b^|I!!-&A;Q_j<@ksGhg z_+~wSSQ@T(7$RMZxp=D*v4D z-v6|L >tB@XtNnArAK#+?S(|^<10 RkcF}imB>egLf-?09MZ*6GY7`n0Prf+Zh&duMw z<<{?g|F$3e@JF}*_$NQze8-(X`}r^Kx_iqne|68jzy8f{xBl0C_doF9Ll1A;{>Y<` zJ^sY+ns@Bnwfo6Edt3HB_4G5(KKK0o0|#Gt@uinvIrQplufOs8H{WXg!`pv+=TCqB zi`DjS`+M(y@YjwH|MvHfK0bWp=qI0k_BpC+{>KcO6Ek4G5`*U7UH*S}`u}74|04$3 ziQP4W?B8AfSk8mxfZq9y;9F$LoF6iZ-M*Xnj$BLJ)Z?4mzunw7_4wuvcsKW(dwhSl z$G1FL8JV6uYZ>`1(kHT}ZpO$-{CTAguW@mCWl7c53j#%fa`>UxFRCrAnYZkU(&9jF z^bUmD*u3VdRH *`q0Mc+_&!}WE8Vq;m+tzW+$!l$R#71V7|Zk0AZqhN6z z>opd21qB-j>P@TLP)8`mvaYPG% X6^@^t?zN?XK!meeS#+g*)&@!_eR(BCFW1F#!gsk>1p~c#u=CgD4_bbS zzeUuG!zXcg%f-};a3_RUA-hr8K?uJ?ILL Q+pNIj<;)4aPup!stnXrRd~ya zDoZL#YrH+n*;RilN&{41dB9s-RZ{A$TJEiOc=Zy~B+^}laek9&Kegm&GVMTeF&Q`6 z)jPkORn>Gb(=trW6Yt8E 6X0`$U sb$wOqb8}>qxrm+(r5?Db-CO(vLS-D}-6JaPCBN zVjSsTr#yblcyEzi3TZ`=p-JI*|D(o3+KP&*t0iIy-J>}eq8%5mdyV!;rI&PyYE}fL z!fU;0rB^Xhl`r>}uB;BMKJ_1`w~VG{4`M}Rw77`Y;524wu-=uWE351y!O?b49IZ!G z>4#o*ydC_r1=$O3T{GeF-?yBX^Mk`lj~;vLYw0eEI_K=AGC$QWy_iP0dMW2+GEvno ztu0?!T~T_uGY&5;DX$GI4V*b`Qgw+Lhz*%e_*dfYKhUiPmL#fy(-PFc`JVkr%?Z_S z%rWu;cY2k25|bqY{rsNtD)lDD`R;#Gj5=w`;OdmZLFp1k;@dY$slQ{sW`}VNjaNeh zNopu*3|*L@hEC(VCZ&1k#H8sXcYD;ZKtDC4B#HDBm1k;vO`q17{ZYcqSi>9$aK*={ zc*5XP?MiT|1WM)_6t4zN^Qb{nk~{jfChm`Kc2~z0_9^HuY3(MB0I;MlX}Q(V`6>II zytSOJ)E_VbCvUv(5kq|ahsUbnvs0T*NtAN@Z |uz2brSq&?pKBo0k!)_k5e?W6`fh#p$rBZLH)LSZbkUC%6 zSN9*(M-3`*QwMQU2fDpTxpHSJwFDC`SDz@=XMWU|){ErtGH%9vgn7r#PZaF4AsFYo zHyRe7%Xu-zNvnVVKB_-?>_0_XaD1Udt9!DPdLHxFFGz@AU)`Sis`&YR!uj6j<4k?F zQbRvC(1o6)L|1?1@+K;8Nq^;Cn5?|e#alDH MYWcpDQj(#kqc@`;E{~o8&% x%-G@%@t4 zZify%esd{8`b!yWoIFS!)kLKa9qA@b_Tn{N{Ym @RUni3*Pi z*Oe%BD`usgrpcG-A5I&c%QB(>v%&UL3NH6Iw?yW13T rdLxd&{Xi z1Z14Bavf_KCLDG^j2bX4Ne#F;p}?j4qutMj$D2B&Zim-&)t^JF*RMb`(3L2N?VgA9 zp%WA6D;KF@3k&Ek^VBfc`O4HhnOVblL8e^86V&iPD(zzk?PIVS?i!#>uf$D{iS%#k zb13y`_wVNZCuldnLJs9*1ZA9dWBNP&yu=<)=cjZ;_V?v1xqgNDi=FR@;JYwG>^|U1 zajO)@mK4U86xveCl>W{AkGI?J(BWq=>i>Y5;)K`vC+!l(*@fY8w%OGq|1KF{Ih1e> zaWlsERYMj6sk oRm1Nj|E>M^dzzD~6AKg4<7vbFWlUo18OFRcY|4-h zLpxLF(oeRs6M7rtJ|-~{mmaGaqsUL{G`C8fV)sQU7jaO=Rx`VGjSWBk9%BQhD-Oa@ zC#lp)Ds&-^>Y?cgYUH%L)JWIus{3q1qSW>N7}6djeX}2ZGl{;Ls0Q7fT&-!bFrG1h zaey(v_+j26e}l;1p!v2R>d?curTyss>el_Wuh5P$$*F_ITTyR_DWDDny2i$ Lh+95aM;2Ttu*(=%LpIGl%Y{gmgvglZ>USHCFLZ%Vv)(e0)u>`AZ3pI2%J zM%s$N{zKwvgRC_e2Zqca*x|GWhenGIDD_9oqc)99AB$K=F#kGzOyb;gkn!mSrCxPt zdNO1E%?Yi2_s2EIR>u@Z7eu8CO}l8(HNOu%GeM1;_KoOquI16awJGl~^7|$2_6My> zJ&keN?TO~TEB~O>Z!yl?XWDWJZTV}xw&fPatuIS=`}<10k8#p Vm~)T#81>lyP;k5VVO8qHdferUe&1l`l!_)F}g66srs z^UeCuH8N3+4D?qcOOol+{nW^=G2dS6bQ?cfSp%IYudR~Tp;Hso=s>A!bV-S8^t58v zXxG z7)@6QM zrV8#-&5pb~Ulw+oqq_XqUN!iSe7vE{f8^s09sak;$B%SHii0+};JeN-{GmK{)Qi=G zm<6T6AS@^flr2`*@)gOgg?nc>xN3` {{{b*X*tc{w}+L*u_QVfw@&R z3t%) y6x>0Nv!l^KXP`BFU4aekD>Pi!;#1xt_TfT*hog?g9rEU?5EC__%Kb0~_J{PX8 zE>)T0I;X0#wyL6ZPN1g3#8RU!)%L-f8ki>83 zj#*S$rkg}b&Z=TWzX=Zkh*YWjrJN^pj*8B$%`ROQT(P3Grl6*@7GkJVV&(@bE-t5% ziYgXW!nb0-Gg9pGs;aIGR?mf1E(wrnVG5;+%bcQW O89(N@`42punm8KtTHlJ;YI8{#E8#scxLDh2n=VTL +@7t?@rvs7y&4dY@6qz+O86{UfmROHZWK}9L@ z{F9^e=HwSu(~4 eHm z>RPTqEG#FTT1inb^=*565sSsj7oAsCRFYS|tcEKOl=?N@2IiLO_3<~_LlMN!&ee&RkD tBlgoV z^39a1zd26P-%M*d%zWE^femGLk@zpcNZKrZb-0y4FNUc}4acy+)cKcki2pi_ M`QpfRX$lAEPCLe`0^%0hIjx93$!7jS+tjW28*aVZ{9vjJT&l6rqn8q07Ja zmwdvXN!NSA-@i6r|F>d4vGASA!HI>x{%_^*U!Tqin}9t_pRfsd|MhwMH>B{tyh#+~ znDv({Dn<_ =`)vOY;s5zN-?{T7^`|?nJ2~j=@e9X)?HxMAMNB9cz4rCjyz27Tu6S)q z58sT(FC2Qa^%JGexYmS3RaWPm2w#5t-buC%vurrih8Z@TX2WzFrrFSI!&Do(ZFsbg zq4Rq-Y_;JVHauj*7j3xThR@ir#fH0W*lfecY`D#a57=<44Y%0vHXGh(!v-5V@vpJJ z12(L%VWAC|*wAmo3>&7~@N^q`ZRob)(O6UNzD)S82s(Gz_LdD>ZFtCr`)$}_!)6<9 zwc%zPZnEJj8y4EIz=jz%Ot)d04ZSu@wPCUi-8NJ 67^?HGPnht$A)*?=`K|O{LVnuoY>z2TssI^0Ps5CKFk~7 z&j6E9R9ctjQiFiYFk8mDR0%L`2)ujz2%N`-=uO}Sz@=>5mx2pCG*YPtzy-dIkvNr? z^BzpW7?<(_zrZX6SED%3!bn;HVC-n(#NG|e!PJqi==^LH96vV#Cyp_AI&kh-(!#$V z*ou*~1b%OvDeq<=dcbs8fp=rX&lX_9cw?UkoMq!J!23@{R~d0W0PMtkB>6c_snalu z{G1LfJ{=x`&;*z;k>Y_T0#C&hh#%nBXaq~ZmjZWUq%6CE?_wkm9|6xzM=lThEZ{dW zLgzKWUt`42R^Z4plzNPp8@<4DFcN WNV zux2J@!A}4 ;->+am1XP&M*H9i5q}Ku zo3qhD1il7%6GrmC3HTbDjxy{;R_WCo@+mlQyB`@O@W+4y&nHgsrNA{92`lh+8yEOC zM)IaEpqerJ@t+R#V-A5A058J40bU3!!nA^y0H^06j|-jwtipT*UJZ=TC;!x4B9Lo1 zDj+X#0x!l$9+m+AhLL*z2v`SmOz0`F`cmq0Jn;ZeTS`9#KOOiOW+Ax1GcKp!flmVt zDB_F}96fnzCPw0~Sf