Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

create_fake_backend: tool for creating fake backend files and settings #8466

Closed
wants to merge 6 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions tools/create_fake_backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
#!/usr/bin/env python3

# This code is part of Qiskit.
#
# (C) Copyright IBM 2022.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Utility script to create fake backends"""

import argparse
import sys
from string import Template
import os
from reno.utils import get_random_string

from update_fake_backends import DEFAULT_DIR
from qiskit import IBMQ

RENO_DIR = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"releasenotes",
"notes",
)

HEADER = """# This code is part of Qiskit.
#
# (C) Copyright IBM 2022.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
"""

init_py = Template(
"""${HEADER}
\"\"\"Fake ${capital_backend_name} device (${no_qubits} qubits)\"\"\"

from .fake_${backend_name} import Fake${capital_backend_name}
"""
)

fake_be_py = Template(
"""${HEADER}

\"\"\"
Fake ${capital_backend_name} device (${no_qubits} qubits).
\"\"\"

import os
from qiskit.providers.fake_provider import fake_backend


class Fake${capital_backend_name}(fake_backend.FakeBackendV2):
\"\"\"A fake ${no_qubits} qubit backend.\"\"\"

dirname = os.path.dirname(__file__)
conf_filename = "conf_${backend_name}.json"
props_filename = "props_${backend_name}.json"
defs_filename = "defs_${backend_name}.json"
backend_name = "fake_${backend_name}"

"""
)

reno_template = Template(
"""---
features:
- |
The fake backend :class:`~Fake${capital_backend_name}` was added with the information
from IBM Quantum `${system_name}` system.
"""
)


def _insert_line_in_section(
line_to_insert, section, line_in_section_starts_with, backend_dir, file_to_modify
):
tmp_file = os.path.join(backend_dir, "_tmp_.py")
with open(file_to_modify, "r") as original, open(tmp_file, "a+") as destination:
previous_line = ""
in_section = False
for line in original.readlines():
if line.startswith(section):
in_section = True
elif (
in_section
and line.startswith(line_in_section_starts_with)
and previous_line < line_to_insert < line
):
in_section = False
destination.write(line_to_insert)
destination.write(line)
os.replace(tmp_file, file_to_modify)


def _main():
parser = argparse.ArgumentParser(description="Create fake backend")
parser.add_argument("--dir", "-d", type=str, default=DEFAULT_DIR)
parser.add_argument("backend_name", type=str, default=None)
parser.add_argument("system_name", type=str, default=None)
Comment on lines +110 to +111
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we pass in only the system name as argument instead of both backend name and system name? You can stripe off the leading ibm_ or ibmq_ from system name to get the backend name
python ./tools/create_fake_backend.py --hub <...> ibm_oslo instead of
python ./tools/create_fake_backend.py --hub <...> oslo ibm_oslo

parser.add_argument("--project", type=str, default=None)
parser.add_argument("--hub", type=str, default=None)
parser.add_argument("--group", type=str, default=None)
args = parser.parse_args()

backend_dir = os.path.join(args.dir, args.backend_name)
if os.path.isdir(backend_dir):
print(f"ERROR: Directory {backend_dir} already exists")
sys.exit(1)

provider = IBMQ.load_account()
if args.hub or args.group or args.project:
provider = IBMQ.get_provider(hub=args.hub, group=args.group, project=args.project)
ibmq_backend = provider.get_backend(args.system_name)

vars_ = {
"HEADER": HEADER,
"backend_name": args.backend_name,
"capital_backend_name": args.backend_name.capitalize(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there was a discussion that we don't need to add BackendV1 fake backend any more. Ideally I would like to have FakeVigo to be a backend that has the latest backend version, FakeVigoV1 and FakeVigoV2 to have explicit backend version name in the class and FakeVigo is simply FakeVigoV2 and it will be FakeVigoV3 if there is a new backend version.

But at the moment we don't have this distinction. Fake backends without explicit version like FakeVigo are all V1 backends. So I would suggest to add V2 for all the backends that are created using this script to avoid confusion. What do you think?

"no_qubits": len(ibmq_backend.properties().qubits),
"system_name": args.system_name,
}

# Step 1. Create the directory for the backend <backend_dir>
os.mkdir(backend_dir)

# Step 2. <backend_dir>/__init__.py
result = init_py.substitute(vars_)
with open(os.path.join(backend_dir, "__init__.py"), "w") as fd:
fd.write(result)

# Step 3. <backend_dir>/fake_<backend_name>.py
result = fake_be_py.substitute(vars_)
with open(os.path.join(backend_dir, f"fake_{args.backend_name}.py"), "w") as fd:
fd.write(result)

# Step 4. update <backend_dir>/../__init__.py
init_file = os.path.join(backend_dir, "..", "__init__.py")
backend_v2_line = f"from .{vars_['backend_name']} import Fake{vars_['capital_backend_name']}\n"
_insert_line_in_section(backend_v2_line, "# BackendV2", "from", backend_dir, init_file)

# Step 5. update <backend_dir>/../../__init__.py
init_file = os.path.join(backend_dir, "..", "..", "__init__.py")
backend_v2_line = f" Fake{vars_['capital_backend_name']}\n"
_insert_line_in_section(backend_v2_line, "Fake V2 Backends", " ", backend_dir, init_file)

# Step 6. update <backend_dir>/../../fake_provider.py
init_file = os.path.join(backend_dir, "..", "..", "fake_provider.py")
backend_v2_line = f" Fake{vars_['capital_backend_name']}(),\n"
_insert_line_in_section(
backend_v2_line,
"class FakeProviderForBackend",
" Fake",
backend_dir,
init_file,
)

# Step 7. releasenotes/notes/fake_<backend_name>-<random_string>.yaml
result = reno_template.substitute(vars_)
with open(
os.path.join(RENO_DIR, f"fake_{args.backend_name}-{get_random_string()}.yaml"), "w"
) as fd:
fd.write(result)


if __name__ == "__main__":
_main()