From c8e688d03ebd13e765284aab897383304e701046 Mon Sep 17 00:00:00 2001 From: jjmartinezQT <133863373+jjmartinezQT@users.noreply.github.com> Date: Fri, 9 Jun 2023 17:56:52 +0200 Subject: [PATCH] Hackathon/hotfixes (#413) * fix save results * empty programs hotfix * clean sequenes * circuit_to_pulses fix * black formatting * unittest for execute * updated changelog * black formatting * changelog updated * isort test * cleaning unittest * black reformat * fixing unittest for mocking all functions called * another fix * add save_results flag * fix * fix tests --------- Co-authored-by: AlbertMitjans --- CHANGELOG.md | 27 ++++++ interrogate_badge.svg | 6 +- src/qililab/chip/chip.py | 9 ++ src/qililab/execute.py | 2 +- src/qililab/execution/bus_execution.py | 6 +- src/qililab/experiment/experiment.py | 22 +++-- src/qililab/instruments/awg.py | 6 +- src/qililab/instruments/qblox/qblox_module.py | 16 +-- src/qililab/platform/platform.py | 14 --- src/qililab/pulse/circuit_to_pulses.py | 13 +-- .../system_control/readout_system_control.py | 2 +- .../simulated_system_control.py | 6 +- src/qililab/system_control/system_control.py | 8 +- .../unit/instruments/qblox/test_qblox_qcm.py | 6 +- .../unit/instruments/qblox/test_qblox_qrm.py | 8 +- tests/unit/instruments/test_awg.py | 7 +- .../test_awg_analog_digital_converter.py | 3 + .../test_simulated_system_control.py | 10 +- .../system_controls/test_system_control.py | 6 +- tests/unit/test_execute.py | 97 +++++++++++++++++++ 20 files changed, 206 insertions(+), 68 deletions(-) create mode 100644 tests/unit/test_execute.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e5c325e36..2ece33c09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,33 @@ ### New features since last release +- Added hotfixes for several issues encountered during the hackathon: + https://github.com/qilimanjaro-tech/qililab/pull/413 + + - Save experiments flag for experiments execute method + + ```python + # Option 1 (Default, save_experiment=True) + experiment = Experiment( + platform=platform, circuits=circuits, options=options + ) + experiment.execute() + + # Option 2 (Equivalent to option 1, save_experiment=False) + experiment = Experiment( + platform=platform, circuits=circuits, options=options + ) + experiment.execute(save_experiment=False) + ``` + + - Empty sequences to avoid creating repeated programs. + - Create empty programs for all qubit flux lines to activate calibrated offsets. + - Added method to get qubits from the chip + + ```python + qubits = ql.Chip.qubits() + ``` + - Added to the `draw()` method the option of specifying if you want: - modulation or not, diff --git a/interrogate_badge.svg b/interrogate_badge.svg index c8d6a2861..2cf9af2ba 100644 --- a/interrogate_badge.svg +++ b/interrogate_badge.svg @@ -1,5 +1,5 @@ - interrogate: 98.3% + interrogate: 97.0% @@ -12,8 +12,8 @@ interrogate interrogate - 98.3% - 98.3% + 97.0% + 97.0% diff --git a/src/qililab/chip/chip.py b/src/qililab/chip/chip.py index fcce3fd2f..3bdf97fbf 100644 --- a/src/qililab/chip/chip.py +++ b/src/qililab/chip/chip.py @@ -164,6 +164,15 @@ def to_dict(self): "nodes": [{RUNCARD.NAME: node.name.value} | asdict(node, dict_factory=dict_factory) for node in self.nodes], } + @property + def qubits(self): + """Chip `qubits` property. + + Returns: + list[int]: List of integers containing the indices of the qubits inside the chip. + """ + return [node.qubit_index for node in self.nodes if isinstance(node, Qubit)] + @property def num_qubits(self) -> int: """Chip 'num_qubits' property diff --git a/src/qililab/execute.py b/src/qililab/execute.py index b6b96eb08..01f2c1f29 100644 --- a/src/qililab/execute.py +++ b/src/qililab/execute.py @@ -51,4 +51,4 @@ def execute(circuit: Circuit, runcard_name: str, nshots=1): # create experiment with options sample_experiment = ql.Experiment(platform=platform, circuits=[circuit], options=options) - return sample_experiment.execute(save_results=False) + return sample_experiment.execute(save_experiment=False, save_results=False) diff --git a/src/qililab/execution/bus_execution.py b/src/qililab/execution/bus_execution.py index 24cfd687b..4510022dd 100644 --- a/src/qililab/execution/bus_execution.py +++ b/src/qililab/execution/bus_execution.py @@ -37,11 +37,11 @@ def compile(self, idx: int, nshots: int, repetition_duration: int, num_bins: int def upload(self): """Uploads any previously compiled program into the instrument.""" - self.system_control.upload() + self.system_control.upload(port=self.port) def run(self): """Run the given pulse sequence.""" - return self.system_control.run() + return self.system_control.run(port=self.port) def setup(self): """Generates the sequence for each bus and uploads it to the sequencer""" @@ -67,7 +67,7 @@ def acquire_result(self) -> Result: f"The bus {self.bus.alias} needs a readout system control to acquire the results. This bus " f"has a {self.system_control.name} instead." ) - return self.system_control.acquire_result() # type: ignore # pylint: disable=no-member + return self.system_control.acquire_result(port=self.port) # type: ignore # pylint: disable=no-member def acquire_time(self, idx: int = 0) -> int: """BusExecution 'acquire_time' property. diff --git a/src/qililab/experiment/experiment.py b/src/qililab/experiment/experiment.py index 5a6845621..fd1c2bcec 100644 --- a/src/qililab/experiment/experiment.py +++ b/src/qililab/experiment/experiment.py @@ -67,7 +67,7 @@ def build_execution(self): # Build ``ExecutionManager`` class self.execution_manager = EXECUTION_BUILDER.build(platform=self.platform, pulse_schedules=self.pulse_schedules) - def run(self, save_results=True) -> Results: + def run(self, save_experiment=True, save_results=True) -> Results: """This method is responsible for: * Creating the live plotting (if connection is provided). * Preparing the `Results` class and the `results.yml` file. @@ -93,7 +93,9 @@ def run(self, save_results=True) -> Results: raise ValueError("Please build the execution_manager before running an experiment.") # Prepares the results - self.results, self.results_path = self.prepare_results(save_results=save_results) + self.results, self.results_path = self.prepare_results( + save_experiment=save_experiment, save_results=save_results + ) data_queue: Queue = Queue() # queue used to store the experiment results self._asynchronous_data_handling(queue=data_queue) @@ -174,7 +176,7 @@ def disconnect(self): """Disconnects from the instruments and releases the device.""" self.platform.disconnect() - def execute(self, save_results=True) -> Results: + def execute(self, save_experiment=True, save_results=True) -> Results: """Runs the whole execution pipeline, which includes the following steps: * Connect to the instruments. @@ -194,7 +196,7 @@ def execute(self, save_results=True) -> Results: self.initial_setup() self.build_execution() self.turn_on_instruments() - results = self.run(save_results=save_results) + results = self.run(save_experiment=save_experiment, save_results=save_results) self.turn_off_instruments() self.disconnect() QcodesInstrument.close_all() @@ -457,7 +459,7 @@ def repetition_duration(self): """ return self.options.settings.repetition_duration - def prepare_results(self, save_results=True) -> tuple[Results, Path | None]: + def prepare_results(self, save_experiment=True, save_results=True) -> tuple[Results, Path | None]: """Creates the ``Results`` class, creates the ``results.yml`` file where the results will be saved, and dumps the experiment data into this file. @@ -476,14 +478,14 @@ def prepare_results(self, save_results=True) -> tuple[Results, Path | None]: loops=self.options.loops, ) - if save_results: + if save_results or save_experiment: # Create the folders & files needed to save the results locally results_path = self._path_to_results_folder() self._create_results_file(results_path) - - # Dump the experiment data into the created file - with open(file=results_path / EXPERIMENT_FILENAME, mode="w", encoding="utf-8") as experiment_file: - yaml.dump(data=self.to_dict(), stream=experiment_file, sort_keys=False) + if save_experiment: + # Dump the experiment data into the created file + with open(file=results_path / EXPERIMENT_FILENAME, mode="w", encoding="utf-8") as experiment_file: + yaml.dump(data=self.to_dict(), stream=experiment_file, sort_keys=False) else: results_path = None diff --git a/src/qililab/instruments/awg.py b/src/qililab/instruments/awg.py index 68172ebdf..eefb20ae6 100644 --- a/src/qililab/instruments/awg.py +++ b/src/qililab/instruments/awg.py @@ -67,9 +67,13 @@ def compile( """ @abstractmethod - def run(self): + def run(self, port: int): """Run the uploaded program""" + @abstractmethod + def upload(self, port: int): + """Upload compiled program.""" + @property def num_sequencers(self): """Number of sequencers in the AWG diff --git a/src/qililab/instruments/qblox/qblox_module.py b/src/qililab/instruments/qblox/qblox_module.py index 84945b8c1..a69c3d4bf 100644 --- a/src/qililab/instruments/qblox/qblox_module.py +++ b/src/qililab/instruments/qblox/qblox_module.py @@ -92,6 +92,7 @@ def __init__(self, settings: dict): def initial_setup(self): """Initial setup""" self._map_outputs() + self.clear_cache() for sequencer in self.awg_sequencers: sequencer_id = sequencer.identifier # Set `sync_en` flag to False (this value will be set to True if the sequencer is used in the execution) @@ -162,9 +163,9 @@ def _compile(self, pulse_bus_schedule: PulseBusSchedule, sequencer: AWGQbloxSequ self.sequences[sequencer.identifier] = (sequence, False) return sequence - def run(self): + def run(self, port: int): """Run the uploaded program""" - self.start_sequencer() + self.start_sequencer(port=port) def _translate_pulse_bus_schedule(self, pulse_bus_schedule: PulseBusSchedule, sequencer: AWGQbloxSequencer): """Translate a pulse sequence into a Q1ASM program and a waveform dictionary. @@ -268,9 +269,10 @@ def _append_acquire_instruction( ): """Append an acquire instruction to the loop.""" - def start_sequencer(self): + def start_sequencer(self, port: int): """Start sequencer and execute the uploaded instructions.""" - for sequencer in self.awg_sequencers: + sequencers = self.get_sequencers_from_chip_port_id(chip_port_id=port) + for sequencer in sequencers: if sequencer.identifier in self.sequences: self.device.arm_sequencer(sequencer=sequencer.identifier) self.device.start_sequencer(sequencer=sequencer.identifier) @@ -486,6 +488,7 @@ def turn_on(self): def clear_cache(self): """Empty cache.""" self._cache = {} + self.sequences = {} @Instrument.CheckDeviceInitialized def reset(self): @@ -493,13 +496,14 @@ def reset(self): self.clear_cache() self.device.reset() - def upload(self): + def upload(self, port: int): """Upload all the previously compiled programs to its corresponding sequencers. This method must be called after the method ``compile``.""" if self.nshots is None or self.repetition_duration is None: raise ValueError("Please compile the circuit before uploading it to the device.") - for sequencer in self.awg_sequencers: + sequencers = self.get_sequencers_from_chip_port_id(chip_port_id=port) + for sequencer in sequencers: if (seq_idx := sequencer.identifier) in self.sequences: sequence, uploaded = self.sequences[seq_idx] self.device.sequencers[seq_idx].sync_en(True) diff --git a/src/qililab/platform/platform.py b/src/qililab/platform/platform.py index 502e2b76d..f5a06bc7a 100644 --- a/src/qililab/platform/platform.py +++ b/src/qililab/platform/platform.py @@ -28,8 +28,6 @@ def __init__(self, runcard_schema: RuncardSchema, connection: API | None = None) self.schema = Schema(**asdict(runcard_schema.schema)) self.connection = connection self._connected_to_instruments: bool = False - self._initial_setup_applied: bool = False - self._instruments_turned_on: bool = False def connect(self, manual_override=False): """Blocks the given device and connects to the instruments. @@ -53,29 +51,17 @@ def connect(self, manual_override=False): def initial_setup(self): """Set the initial setup of the instruments""" - if self._initial_setup_applied: - logger.info("Initial setup already applied to the instruments") - return self.instrument_controllers.initial_setup() - self._initial_setup_applied = True logger.info("Initial setup applied to the instruments") def turn_on_instruments(self): """Turn on the instruments""" - if self._instruments_turned_on: - logger.info("Instruments already turned on") - return self.instrument_controllers.turn_on_instruments() - self._instruments_turned_on = True logger.info("Instruments turned on") def turn_off_instruments(self): """Turn off the instruments""" - if not self._instruments_turned_on: - logger.info("Instruments already turned off") - return self.instrument_controllers.turn_off_instruments() - self._instruments_turned_on = False logger.info("Instruments turned off") def disconnect(self): diff --git a/src/qililab/pulse/circuit_to_pulses.py b/src/qililab/pulse/circuit_to_pulses.py index ca10e05de..299f5a798 100644 --- a/src/qililab/pulse/circuit_to_pulses.py +++ b/src/qililab/pulse/circuit_to_pulses.py @@ -125,12 +125,13 @@ def translate(self, circuits: list[Circuit]) -> list[PulseSchedule]: self._update_time(time=time, qubit_idx=gate.control_qubits[0], pulse_time=pad_time, wait_time=0) if pulse_event is not None: # this happens for the Identity gate pulse_schedule.add_event(pulse_event=pulse_event, port=port) - with contextlib.suppress(ValueError): - # If we find a flux port, create empty schedule for that port - for qubit_idx in gate.qubits: - flux_port = chip.get_port_from_qubit_idx(idx=qubit_idx, line=Line.FLUX) - if flux_port is not None: - pulse_schedule.create_schedule(port=flux_port) + + for qubit in chip.qubits: + with contextlib.suppress(ValueError): + # If we find a flux port, create empty schedule for that port + flux_port = chip.get_port_from_qubit_idx(idx=qubit, line=Line.FLUX) + if flux_port is not None: + pulse_schedule.create_schedule(port=flux_port) pulse_schedule_list.append(pulse_schedule) diff --git a/src/qililab/system_control/readout_system_control.py b/src/qililab/system_control/readout_system_control.py index 01de6dbed..cb425a198 100644 --- a/src/qililab/system_control/readout_system_control.py +++ b/src/qililab/system_control/readout_system_control.py @@ -13,7 +13,7 @@ class ReadoutSystemControl(SystemControl): name = SystemControlName.READOUT_SYSTEM_CONTROL - def acquire_result(self) -> Result: + def acquire_result(self, port: int) -> Result: """Read the result from the vector network analyzer instrument Returns: diff --git a/src/qililab/system_control/simulated_system_control.py b/src/qililab/system_control/simulated_system_control.py index 1b26af628..c8ef71954 100644 --- a/src/qililab/system_control/simulated_system_control.py +++ b/src/qililab/system_control/simulated_system_control.py @@ -70,12 +70,12 @@ def __str__(self): """String representation of a Simulated SystemControl class.""" return "--" - def run(self) -> None: + def run(self, port: int) -> None: """Run the program""" self._evo.set_pulse_sequence(pulse_sequence=self.sequence, resolution=self.settings.resolution * 1e-9) self._evo.evolve() - def acquire_result(self) -> SimulatorResult: + def acquire_result(self, port: int) -> SimulatorResult: """Read the result from the AWG instrument Returns: @@ -110,5 +110,5 @@ def compile( self.sequence = [i_waveform] return [] - def upload(self): + def upload(self, port: int): pass diff --git a/src/qililab/system_control/system_control.py b/src/qililab/system_control/system_control.py index cdd125f52..4ecbc5ac8 100644 --- a/src/qililab/system_control/system_control.py +++ b/src/qililab/system_control/system_control.py @@ -63,22 +63,22 @@ def compile( "sequence." ) - def upload(self): + def upload(self, port: int): """Uploads any previously compiled program into the instrument.""" for instrument in self.instruments: if isinstance(instrument, AWG): - instrument.upload() + instrument.upload(port=port) return raise AttributeError( f"The system control with alias {self.settings.alias} doesn't have any AWG to upload a program." ) - def run(self) -> None: + def run(self, port: int) -> None: """Runs any previously uploaded program into the instrument.""" for instrument in self.instruments: if isinstance(instrument, AWG): - instrument.run() + instrument.run(port=port) return raise AttributeError( diff --git a/tests/unit/instruments/qblox/test_qblox_qcm.py b/tests/unit/instruments/qblox/test_qblox_qcm.py index 80f4b03bb..b94afbc02 100644 --- a/tests/unit/instruments/qblox/test_qblox_qcm.py +++ b/tests/unit/instruments/qblox/test_qblox_qcm.py @@ -132,7 +132,7 @@ def test_inital_setup_method(self, qcm: QbloxQCM): def test_start_sequencer_method(self, qcm: QbloxQCM): """Test start_sequencer method""" - qcm.start_sequencer() + qcm.start_sequencer(port=0) qcm.device.arm_sequencer.assert_not_called() qcm.device.start_sequencer.assert_not_called() @@ -211,12 +211,12 @@ def test_compile(self, qcm, pulse_bus_schedule): def test_upload_raises_error(self, qcm): """Test upload method raises error.""" with pytest.raises(ValueError, match="Please compile the circuit before uploading it to the device"): - qcm.upload() + qcm.upload(port=0) def test_upload_method(self, qcm, pulse_bus_schedule): """Test upload method""" qcm.compile(pulse_bus_schedule, nshots=1000, repetition_duration=100, num_bins=1) - qcm.upload() + qcm.upload(port=pulse_bus_schedule.port) qcm.device.sequencer0.sequence.assert_called_once() qcm.device.sequencer0.sync_en.assert_called_once_with(True) diff --git a/tests/unit/instruments/qblox/test_qblox_qrm.py b/tests/unit/instruments/qblox/test_qblox_qrm.py index efac4157c..eac92f51a 100644 --- a/tests/unit/instruments/qblox/test_qblox_qrm.py +++ b/tests/unit/instruments/qblox/test_qblox_qrm.py @@ -271,7 +271,7 @@ def test_double_scope_forbidden(self, qrm_two_scopes: QbloxQRM): def test_start_sequencer_method(self, qrm: QbloxQRM): """Test start_sequencer method""" - qrm.start_sequencer() + qrm.start_sequencer(port=1) qrm.device.arm_sequencer.assert_not_called() qrm.device.start_sequencer.assert_not_called() @@ -409,7 +409,7 @@ def test_cache_multiplexing(self, qrm, multiplexed_pulse_bus_schedule: PulseBusS def test_acquisition_data_is_removed_when_calling_compile_twice(self, qrm, pulse_bus_schedule): """Test that the acquisition data of the QRM device is deleted when calling compile twice.""" sequences = qrm.compile(pulse_bus_schedule, nshots=1000, repetition_duration=100, num_bins=1) - qrm.upload() + qrm.upload(port=pulse_bus_schedule.port) sequences2 = qrm.compile(pulse_bus_schedule, nshots=1000, repetition_duration=100, num_bins=1) assert len(sequences) == 1 assert len(sequences2) == 1 @@ -419,13 +419,13 @@ def test_acquisition_data_is_removed_when_calling_compile_twice(self, qrm, pulse def test_upload_raises_error(self, qrm): """Test upload method raises error.""" with pytest.raises(ValueError, match="Please compile the circuit before uploading it to the device"): - qrm.upload() + qrm.upload(port=1) def test_upload_method(self, qrm, pulse_bus_schedule): """Test upload method""" pulse_bus_schedule.port = 1 qrm.compile(pulse_bus_schedule, nshots=1000, repetition_duration=100, num_bins=1) - qrm.upload() + qrm.upload(port=pulse_bus_schedule.port) qrm.device.sequencers[0].sync_en.assert_called_once_with(True) qrm.device.sequencers[1].sequence.assert_not_called() diff --git a/tests/unit/instruments/test_awg.py b/tests/unit/instruments/test_awg.py index 74923a890..c12681246 100644 --- a/tests/unit/instruments/test_awg.py +++ b/tests/unit/instruments/test_awg.py @@ -10,12 +10,17 @@ class DummyAWG(AWG): """Dummy AWG class.""" - def compile(self, pulse_bus_schedule: PulseBusSchedule, nshots: int, repetition_duration: int) -> list: + def compile( + self, pulse_bus_schedule: PulseBusSchedule, nshots: int, repetition_duration: int, num_bins: int + ) -> list: return [] def run(self): pass + def upload(self, port: int): + pass + @pytest.fixture(name="awg") def fixture_awg(): diff --git a/tests/unit/instruments/test_awg_analog_digital_converter.py b/tests/unit/instruments/test_awg_analog_digital_converter.py index 5cf9be818..d165789f7 100644 --- a/tests/unit/instruments/test_awg_analog_digital_converter.py +++ b/tests/unit/instruments/test_awg_analog_digital_converter.py @@ -21,6 +21,9 @@ def compile( def run(self): pass + def upload(self, port: int): + pass + def acquire_result(self): return [] diff --git a/tests/unit/system_controls/test_simulated_system_control.py b/tests/unit/system_controls/test_simulated_system_control.py index fab640e53..5cea53c67 100644 --- a/tests/unit/system_controls/test_simulated_system_control.py +++ b/tests/unit/system_controls/test_simulated_system_control.py @@ -56,14 +56,14 @@ def test_compile_method( def test_upload_method(self, simulated_system_control: SimulatedSystemControl): """Test upload method.""" - simulated_system_control.upload() # this method does nothing + simulated_system_control.upload(port=0) # this method does nothing def test_run_method(self, simulated_system_control: SimulatedSystemControl, pulse_bus_schedule: PulseBusSchedule): """Test run method.""" simulated_system_control._evo = MagicMock() simulated_system_control.compile(pulse_bus_schedule=pulse_bus_schedule) - simulated_system_control.run() - result = simulated_system_control.acquire_result() + simulated_system_control.run(port=pulse_bus_schedule.port) + result = simulated_system_control.acquire_result(port=pulse_bus_schedule.port) assert isinstance(result, SimulatorResult) def test_name_property(self, simulated_system_control: SimulatedSystemControl): @@ -77,6 +77,6 @@ def test_probabilities( simulated_system_control._evo = MagicMock() simulated_system_control.compile(pulse_bus_schedule=pulse_bus_schedule) simulated_system_control.compile(pulse_bus_schedule=pulse_bus_schedule) - simulated_system_control.run() - result = simulated_system_control.acquire_result() + simulated_system_control.run(port=pulse_bus_schedule.port) + result = simulated_system_control.acquire_result(port=pulse_bus_schedule.port) assert result.probabilities() == {} diff --git a/tests/unit/system_controls/test_system_control.py b/tests/unit/system_controls/test_system_control.py index cdf4103c7..347334f65 100644 --- a/tests/unit/system_controls/test_system_control.py +++ b/tests/unit/system_controls/test_system_control.py @@ -111,7 +111,7 @@ def test_upload_raises_error(self, system_control_without_awg: SystemControl): AttributeError, match="The system control with alias test_alias doesn't have any AWG to upload a program", ): - system_control_without_awg.upload() + system_control_without_awg.upload(port=0) def test_upload(self, system_control: SystemControl, pulse_bus_schedule: PulseBusSchedule): """Test upload method.""" @@ -119,7 +119,7 @@ def test_upload(self, system_control: SystemControl, pulse_bus_schedule: PulseBu assert isinstance(awg, AWG) awg.device = MagicMock() _ = system_control.compile(pulse_bus_schedule, nshots=1000, repetition_duration=2000, num_bins=1) - system_control.upload() + system_control.upload(port=pulse_bus_schedule.port) for seq_idx in range(awg.num_sequencers): awg.device.sequencers[seq_idx].sequence.assert_called_once() @@ -129,7 +129,7 @@ def test_run_raises_error(self, system_control_without_awg: SystemControl): AttributeError, match="The system control with alias test_alias doesn't have any AWG to run a program", ): - system_control_without_awg.run() + system_control_without_awg.run(port=0) class TestProperties: diff --git a/tests/unit/test_execute.py b/tests/unit/test_execute.py new file mode 100644 index 000000000..444359ded --- /dev/null +++ b/tests/unit/test_execute.py @@ -0,0 +1,97 @@ +"""Tests for the Experiment class.""" +import time +from unittest.mock import MagicMock, patch + +import pytest + +from qililab import build_platform +from qililab.execution.execution_manager import ExecutionManager +from qililab.experiment import Experiment +from qililab.platform import Platform +from tests.data import FluxQubitSimulator, simulated_experiment_circuit + + +@pytest.fixture(name="simulated_platform") +@patch("qililab.system_control.simulated_system_control.Evolution", autospec=True) +def fixture_simulated_platform(mock_evolution: MagicMock) -> Platform: + """Return Platform object.""" + + # Mocked Evolution needs: system.qubit.frequency, psi0, states, times + mock_system = MagicMock() + mock_system.qubit.frequency = 0 + mock_evolution.return_value.system = mock_system + mock_evolution.return_value.states = [] + mock_evolution.return_value.times = [] + mock_evolution.return_value.psi0 = None + + with patch( + "qililab.platform.platform_manager_yaml.yaml.safe_load", return_value=FluxQubitSimulator.runcard + ) as mock_load: + with patch("qililab.platform.platform_manager_yaml.open") as mock_open: + platform = build_platform(name="flux_qubit") + mock_load.assert_called() + mock_open.assert_called() + return platform + + +@pytest.fixture(name="simulated_experiment") +def fixture_simulated_experiment(simulated_platform: Platform): + """Return Experiment object.""" + return Experiment(platform=simulated_platform, circuits=[simulated_experiment_circuit]) + + +@patch("qililab.experiment.experiment.open") +@patch("qililab.experiment.experiment.yaml.safe_dump") +@patch("qililab.system_control.simulated_system_control.SimulatedSystemControl.run") +@patch("qililab.experiment.experiment.os.makedirs") +class TestSimulatedExecution: + """Unit tests checking the execution of a simulated platform""" + + def test_execute_without_saving_experiment( + self, + mock_open: MagicMock, + mock_dump: MagicMock, + mock_ssc_run: MagicMock, + mock_makedirs: MagicMock, + simulated_experiment: Experiment, + ): + """Test execute method with simulated qubit""" + + # Method under test + results = simulated_experiment.execute(save_experiment=False) + + time.sleep(0.3) + + # Assert simulator called + mock_ssc_run.assert_called() + + # Test result + with pytest.raises(ValueError): # Result should be SimulatedResult + results.acquisitions() + + def test_execute_with_saving_experiment( + self, + mock_open: MagicMock, + mock_dump: MagicMock, + mock_ssc_run: MagicMock, + mock_makedirs: MagicMock, + simulated_experiment: Experiment, + ): + """Test execute method with simulated qubit""" + + # Method under test + results = simulated_experiment.execute() + + time.sleep(0.3) + + # Assert simulator called + mock_ssc_run.assert_called() + + # Assert called functions + mock_makedirs.assert_called() + mock_open.assert_called() + mock_dump.assert_called() + + # Test result + with pytest.raises(ValueError): # Result should be SimulatedResult + results.acquisitions()