# Copyright 2022 Xanadu Quantum Technologies Inc.# Licensed under the Apache License, Version 2.0 (the "License");# you may not use this file except in compliance with the License.# You may obtain a copy of the License at# http://www.apache.org/licenses/LICENSE-2.0# Unless required by applicable law or agreed to in writing, software# distributed under the License is distributed on an "AS IS" BASIS,# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.# See the License for the specific language governing permissions and# limitations under the License."""The null.qubit device is a no-op device, useful for resource estimation, and forbenchmarking PennyLane's auxiliary functionality outside direct circuit evaluations."""importinspectimportjsonimportloggingimporttimefromcollectionsimportdefaultdictfromdataclassesimportreplacefromfunctoolsimportlru_cache,singledispatchfromnumbersimportNumberfromtypingimportOptional,Unionimportnumpyasnpfrompennylaneimportmathfrompennylane.devices.modifiersimportsimulator_tracking,single_tape_supportfrompennylane.measurementsimport(ClassicalShadowMP,CountsMP,DensityMatrixMP,MeasurementProcess,MeasurementValue,ProbabilityMP,StateMP,)frompennylane.ops.op_mathimportAdjoint,Controlled,ControlledOpfrompennylane.tapeimportQuantumScriptOrBatchfrompennylane.transforms.coreimportTransformProgramfrompennylane.typingimportResult,ResultBatchfrom.importDefaultQubit,Devicefrom.execution_configimportDefaultExecutionConfig,ExecutionConfigfrom.preprocessimportdecomposelogger=logging.getLogger(__name__)logger.addHandler(logging.NullHandler())RESOURCES_FNAME_PREFIX="__pennylane_resources_data_"
[docs]@singledispatchdefzero_measurement(mp:MeasurementProcess,num_device_wires,shots:Optional[int],batch_size:int,interface:str):"""Create all-zero results for various measurement processes."""return_zero_measurement(mp,num_device_wires,shots,batch_size,interface)
def_zero_measurement(mp:MeasurementProcess,num_device_wires:int,shots:Optional[int],batch_size,interface):shape=mp.shape(shots,num_device_wires)ifbatch_sizeisnotNone:shape=(batch_size,)+shapeif"jax"notininterface:return_cached_zero_return(shape,interface,mp.numeric_type)returnmath.zeros(shape,like=interface,dtype=mp.numeric_type)@lru_cache(maxsize=128)def_cached_zero_return(shape,interface,dtype):returnmath.zeros(shape,like=interface,dtype=dtype)@zero_measurement.registerdef_(mp:ClassicalShadowMP,num_device_wires,shots:Optional[int],batch_size,interface):ifbatch_sizeisnotNone:# shapes = [(batch_size,) + shape for shape in shapes]raiseValueError("Parameter broadcasting is not supported with null.qubit and qml.classical_shadow")shape=mp.shape(shots,num_device_wires)returnmath.zeros(shape,like=interface,dtype=np.int8)@zero_measurement.registerdef_(mp:CountsMP,num_device_wires,shots,batch_size,interface):outcomes=[]ifmp.obsisNoneandnotisinstance(mp.mv,MeasurementValue):state="0"*num_device_wiresresults={state:math.asarray(shots,like=interface)}ifmp.all_outcomes:outcomes=[f"{x:0{num_device_wires}b}"forxinrange(1,2**num_device_wires)]else:outcomes=sorted(mp.eigvals())# always assign shots to the smallestresults={outcomes[0]:math.asarray(shots,like=interface)}outcomes=outcomes[1:]ifmp.all_outcomeselse[]ifoutcomes:zero=math.asarray(0,like=interface)forvalinoutcomes:results[val]=zeroifbatch_sizeisnotNone:results=tuple(resultsfor_inrange(batch_size))returnresultszero_measurement.register(DensityMatrixMP)(_zero_measurement)@zero_measurement.register(StateMP)@zero_measurement.register(ProbabilityMP)def_(mp:Union[StateMP,ProbabilityMP],num_device_wires:int,shots:Optional[int],batch_size,interface,):num_wires=len(mp.wires)ornum_device_wiresstate=[1.0]+[0.0]*(2**num_wires-1)ifbatch_sizeisnotNone:state=[state]*batch_sizereturnmath.asarray(state,like=interface)def_interface(config:ExecutionConfig):returnconfig.interface.get_like()ifconfig.gradient_method=="backprop"else"numpy"def_simulate_resource_use(circuit,outfile):num_wires=len(circuit.wires)gate_types=defaultdict(int)foropincircuit.operations:controls=0adj=Falsewhilehasattr(op,"base"):iftype(op)in(Controlled,ControlledOp):# Don't check this with `isinstance` to avoid unrolling ops like CNOTcontrols+=len(op.control_wires)elifisinstance(op,Adjoint):adj=notadjelse:break# Certain gates have "base" but shouldn't be broken down (like CNOT)# NOTE: Pow, Exp, Add, etc. intentionally not handled to be compatible with Catalystop=op.base# Barrier is not a gate and takes no resources, so we skip itifop.name=="Barrier":# (this confuses codecov, despite being tested)continue# pragma: no covername=op.nameifadj:name=f"Adj({name})"ifcontrols:name=f"{controlsifcontrols>1else''}C({name})"gate_types[name]+=1# NOTE: For now, this information is being printed to match the behaviour of catalyst resource tracking.# In the future it may be better to return this information in a more structured way.json.dump({"num_wires":num_wires,"num_gates":sum(gate_types.values()),"gate_types":gate_types,},outfile,)
[docs]@simulator_tracking@single_tape_supportclassNullQubit(Device):"""Null qubit device for PennyLane. This device performs no operations involved in numerical calculations. Instead the time spent in execution is dominated by support (or setting up) operations, like tape creation etc. Args: wires (int, Iterable[Number, str]): Number of wires present on the device, or iterable that contains unique labels for the wires as numbers (i.e., ``[-1, 0, 2]``) or strings (``['aux_wire', 'q1', 'q2']``). Default ``None`` if not specified. shots (int, Sequence[int], Sequence[Union[int, Sequence[int]]]): The default number of shots to use in executions involving this device. track_resources (bool): If True, track the number of resources used by the device and save them to a JSON file. The filename will match ``__pennylane_resources_data_*.json`` where the wildcard (asterisk) is replaced by the timestamp of when execution began in nanoseconds since Unix EPOCH. This argument is experimental and subject to change. **Example:** .. code-block:: python qs = qml.tape.QuantumScript( [qml.Hadamard(0), qml.CNOT([0, 1])], [qml.expval(qml.PauliZ(0)), qml.probs()], ) qscripts = [qs, qs, qs] >>> dev = NullQubit() >>> program, execution_config = dev.preprocess() >>> new_batch, post_processing_fn = program(qscripts) >>> results = dev.execute(new_batch, execution_config=execution_config) >>> post_processing_fn(results) ((array(0.), array([1., 0., 0., 0.])), (array(0.), array([1., 0., 0., 0.])), (array(0.), array([1., 0., 0., 0.]))) This device currently supports (trivial) derivatives: >>> from pennylane.devices import ExecutionConfig >>> dev.supports_derivatives(ExecutionConfig(gradient_method="device")) True This device can be used to track resource usage: .. code-block:: python n_layers = 50 n_wires = 100 shape = qml.StronglyEntanglingLayers.shape(n_layers=n_layers, n_wires=n_wires) @qml.qnode(dev) def circuit(params): qml.StronglyEntanglingLayers(params, wires=range(n_wires)) return [qml.expval(qml.Z(i)) for i in range(n_wires)] params = np.random.random(shape) with qml.Tracker(dev) as tracker: circuit(params) >>> tracker.history["resources"][0] num_wires: 100 num_gates: 10000 depth: 502 shots: Shots(total=None) gate_types: {'Rot': 5000, 'CNOT': 5000} gate_sizes: {1: 5000, 2: 5000} .. details:: :title: Tracking ``NullQubit`` tracks: * ``executions``: the number of unique circuits that would be required on quantum hardware * ``shots``: the number of shots * ``resources``: the :class:`~.resource.Resources` for the executed circuit. * ``simulations``: the number of simulations performed. One simulation can cover multiple QPU executions, such as for non-commuting measurements and batched parameters. * ``batches``: The number of times :meth:`~.execute` is called. * ``results``: The results of each call of :meth:`~.execute` * ``derivative_batches``: How many times :meth:`~.compute_derivatives` is called. * ``execute_and_derivative_batches``: How many times :meth:`~.execute_and_compute_derivatives` is called * ``vjp_batches``: How many times :meth:`~.compute_vjp` is called * ``execute_and_vjp_batches``: How many times :meth:`~.execute_and_compute_vjp` is called * ``jvp_batches``: How many times :meth:`~.compute_jvp` is called * ``execute_and_jvp_batches``: How many times :meth:`~.execute_and_compute_jvp` is called * ``derivatives``: How many circuits are submitted to :meth:`~.compute_derivatives` or :meth:`~.execute_and_compute_derivatives`. * ``vjps``: How many circuits are submitted to :meth:`~.compute_vjp` or :meth:`~.execute_and_compute_vjp` * ``jvps``: How many circuits are submitted to :meth:`~.compute_jvp` or :meth:`~.execute_and_compute_jvp` """@propertydefname(self):"""The name of the device."""return"null.qubit"def__init__(self,wires=None,shots=None,track_resources=False)->None:super().__init__(wires=wires,shots=shots)self._debugger=Noneself._track_resources=track_resources# this is required by Catalyst to toggle the tracker at runtimeself.device_kwargs={"track_resources":track_resources}def_simulate(self,circuit,interface):num_device_wires=len(self.wires)ifself.wireselselen(circuit.wires)results=[]ifself._track_resources:timestamp=int(time.time()*1e9)# nanoseconds since epochresources_fname=f"{RESOURCES_FNAME_PREFIX}{timestamp}.json"withopen(resources_fname,"x",encoding="utf-8")asf:_simulate_resource_use(circuit,f)forsincircuit.shotsor[None]:r=tuple(zero_measurement(mp,num_device_wires,s,circuit.batch_size,interface)formpincircuit.measurements)results.append(r[0]iflen(circuit.measurements)==1elser)ifcircuit.shots.has_partitioned_shots:returntuple(results)returnresults[0]def_derivatives(self,circuit,interface):shots=circuit.shotsnum_device_wires=len(self.wires)ifself.wireselselen(circuit.wires)n=len(circuit.trainable_params)derivatives=tuple((math.zeros_like(zero_measurement(mp,num_device_wires,shots,circuit.batch_size,interface)),)*nformpincircuit.measurements)ifn==1:derivatives=tuple(d[0]fordinderivatives)returnderivatives[0]iflen(derivatives)==1elsederivatives@staticmethoddef_vjp(circuit,interface):batch_size=circuit.batch_sizen=len(circuit.trainable_params)res_shape=(n,)ifbatch_sizeisNoneelse(n,batch_size)returnmath.zeros(res_shape,like=interface)@staticmethoddef_jvp(circuit,interface):jvps=(math.asarray(0.0,like=interface),)*len(circuit.measurements)returnjvps[0]iflen(jvps)==1elsejvps@staticmethoddef_setup_execution_config(execution_config:ExecutionConfig)->ExecutionConfig:"""No-op function to allow for borrowing DefaultQubit.preprocess without AttributeErrors"""returnexecution_config@propertydef_max_workers(self):"""No-op property to allow for borrowing DefaultQubit.preprocess without AttributeErrors"""returnNone# pylint: disable=cell-var-from-loop
[docs]defexecute(self,circuits:QuantumScriptOrBatch,execution_config:ExecutionConfig=DefaultExecutionConfig,)->Union[Result,ResultBatch]:iflogger.isEnabledFor(logging.DEBUG):# pragma: no coverlogger.debug("""Entry with args=(circuits=%s) called by=%s""",circuits,"::L".join(str(i)foriininspect.getouterframes(inspect.currentframe(),2)[1][1:3]),)returntuple(self._simulate(c,_interface(execution_config))forcincircuits)