Skip to content

Commit 1706d94

Browse files
kt474ElePT
andauthored
Update instance selection behavior (#2375)
* Filter by pricing type * Add remaining warnings * Update init warning msg * Add release note & backend warning * Fix unit tests * Update tests & refactor * Integration tests * Update msg wording * Update unit tests * Formatting * Make channel optional * Prioritize free/trial instances * Add end paratheses * Fix lint & typing * Fix black * Apply suggestion from @ElePT Co-authored-by: Elena Peña Tapia <[email protected]> * Apply suggestion from @ElePT Co-authored-by: Elena Peña Tapia <[email protected]> * Remove extra space --------- Co-authored-by: Elena Peña Tapia <[email protected]>
1 parent 46948d9 commit 1706d94

File tree

7 files changed

+158
-27
lines changed

7 files changed

+158
-27
lines changed

qiskit_ibm_runtime/accounts/account.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,12 +350,16 @@ def list_instances(self) -> List[Dict[str, Any]]:
350350
plan_name = (
351351
catalog_result.get("overview_ui", {}).get("en", {}).get("display_name", "")
352352
)
353+
pricing_type = (
354+
catalog_result.get("metadata", {}).get("pricing", {}).get("type", "")
355+
)
353356
crns.append(
354357
{
355358
"crn": item.get("crn"),
356359
"plan": plan_name.lower(),
357360
"name": item.get("name"),
358361
"tags": item.get("tags"),
362+
"pricing_type": pricing_type.lower(),
359363
}
360364
)
361365

qiskit_ibm_runtime/accounts/management.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,9 @@ def get(
180180
saved_account = read_config(filename=filename, name=name)
181181
if not saved_account:
182182
raise AccountNotFoundError(f"Account with the name {name} does not exist on disk.")
183+
logger.warning("Loading saved account: %s", name)
183184
return Account.from_saved_format(saved_account)
184-
185+
logger.warning("Loading default saved account")
185186
channel_ = channel or os.getenv("QISKIT_IBM_CHANNEL") or _DEFAULT_CHANNEL_TYPE
186187
env_account = cls._from_env_variables(channel_)
187188
if env_account is not None:

qiskit_ibm_runtime/qiskit_runtime_service.py

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,9 @@ def __init__(
109109
``token``. The ``local`` channel doesn't require authentication.
110110
For non-local channels, it is recommended to always provide the relevant ``instance``
111111
to minimize API calls. If an ``instance`` is not defined, the service will fetch all
112-
instances accessible within the
113-
account, filtered by ``region``, ``plans_preference``, and ``tags``.
112+
instances accessible within the account, filtered by ``region``, ``plans_preference``,
113+
and ``tags``. If ``plans_preference`` is not set, free and trial instances will be prioritized
114+
over paid instances.
114115
115116
The service will attempt to load an account from file if (a) no explicit ``token``
116117
was provided during instantiation or (b) a ``name`` is specified, even if an explicit
@@ -231,6 +232,37 @@ def __init__(
231232
else:
232233
self._api_clients = {}
233234
instance_backends = self._resolve_cloud_instances(instance)
235+
instance_names = [instance.get("name") for instance in self._backend_instance_groups]
236+
instance_plan_names = {
237+
instance.get("plan") for instance in self._backend_instance_groups
238+
}
239+
240+
tags_str = ", ".join(self._tags) if self._tags else "None"
241+
region_str = self._region if self._region else "us-east, eu-de"
242+
if self._plans_preference:
243+
joined_preferences: str = ", ".join(self._plans_preference)
244+
plans_preference_str = f", plans_preference: {joined_preferences})"
245+
else:
246+
joined_plan_names = ", ".join(instance_plan_names)
247+
plans_preference_str = f"), and available plans: ({joined_plan_names})"
248+
249+
filters = f"(tags: {tags_str}, " f"region: {region_str}" f"{plans_preference_str}"
250+
251+
logger.warning(
252+
"Instance was not set at service instantiation. %s"
253+
"Based on the following filters: %s, "
254+
"the available account instances are: %s. "
255+
"If you need a specific instance set it explicitly either by "
256+
"using a saved account with a saved default instance or passing it "
257+
"in directly to QiskitRuntimeService().",
258+
(
259+
""
260+
if self._plans_preference
261+
else "Free and trial plan instances will be prioritized. "
262+
),
263+
filters,
264+
", ".join(instance_names),
265+
)
234266
for inst, _ in instance_backends:
235267
self._get_or_create_cloud_client(inst)
236268

@@ -292,6 +324,13 @@ def _filter_instances_by_saved_preferences(self) -> None:
292324
self._backend_instance_groups = sorted(
293325
filtered_groups, key=lambda d: plans.index(d["plan"])
294326
)
327+
else:
328+
# if plans_preference is not set, prioritize free and trial plans
329+
ordered_pricing_types = ["free", "trial", "paygo", "paid", "subscription"]
330+
self._backend_instance_groups = sorted(
331+
self._backend_instance_groups,
332+
key=lambda d: ordered_pricing_types.index(d["pricing_type"]),
333+
)
295334

296335
if not self._backend_instance_groups:
297336
error_string = ""
@@ -349,14 +388,28 @@ def _discover_account(
349388
proxies=proxies,
350389
verify=verify_,
351390
)
391+
logger.warning(
392+
"Loading account with the given token. A saved account will not be used."
393+
)
352394
else:
353395
if url:
354396
logger.warning("Loading default %s account. Input 'url' is ignored.", channel)
355397
account = AccountManager.get(filename=filename, name=name, channel=channel)
356-
elif any([token, url]):
357-
# Let's not infer based on these attributes as they may change in the future.
398+
elif token:
399+
account = Account.create_account(
400+
channel="ibm_quantum_platform",
401+
token=token,
402+
url=url,
403+
instance=instance,
404+
proxies=proxies,
405+
verify=verify_,
406+
)
407+
logger.warning(
408+
"Loading account with the given token. A saved account will not be used."
409+
)
410+
elif url:
358411
raise ValueError(
359-
"'channel' is required if 'token', or 'url' is specified but 'name' is not."
412+
"'url' is not valid as a standalone parameter. Try also passing in 'token' or 'name'."
360413
)
361414

362415
# channel is not defined yet, get it from the AccountManager
@@ -525,9 +578,25 @@ def backends(
525578
if name not in backends_available:
526579
continue
527580
backends_available = [name]
581+
else:
582+
for inst_details in self._backend_instance_groups:
583+
if inst == inst_details["crn"]:
584+
logger.warning(
585+
"Loading instance: %s, plan: %s",
586+
inst_details["name"],
587+
inst_details["plan"],
588+
)
528589
for backend_name in backends_available:
529590
if backend_name in unique_backends:
530591
continue
592+
if name:
593+
for inst_details in self._backend_instance_groups:
594+
if inst == inst_details["crn"]:
595+
logger.warning(
596+
"Using instance: %s, plan: %s",
597+
inst_details["name"],
598+
inst_details["plan"],
599+
)
531600
unique_backends.add(backend_name)
532601
self._get_or_create_cloud_client(inst)
533602
if backend := self._create_backend_obj(
@@ -582,25 +651,20 @@ def _resolve_cloud_instances(self, instance: Optional[str]) -> List[Tuple[str, L
582651
return [(default_crn, self._discover_backends_from_instance(default_crn))]
583652
if not self._all_instances:
584653
self._all_instances = self._account.list_instances()
585-
instance_names = [instance.get("name") for instance in self._all_instances]
586-
logger.warning(
587-
"Instance was not set at service instantiation. A relevant instance from all available "
588-
"account instances will be selected based on the desired action. "
589-
"Available account instances are: %s. "
590-
"If you need the instance to be fixed, set it explicitly.",
591-
", ".join(instance_names),
592-
)
593654
if not self._backend_instance_groups:
594655
self._backend_instance_groups = [
595656
{
657+
"name": inst["name"],
596658
"crn": inst["crn"],
597659
"plan": inst["plan"],
598660
"backends": self._discover_backends_from_instance(inst["crn"]),
599661
"tags": inst["tags"],
662+
"pricing_type": inst["pricing_type"],
600663
}
601664
for inst in self._all_instances
602665
]
603666
self._filter_instances_by_saved_preferences()
667+
604668
return [(inst["crn"], inst["backends"]) for inst in self._backend_instance_groups]
605669

606670
def _get_or_create_cloud_client(self, instance: str) -> None:
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
When :class:`.QiskitRuntimeService` is initialized without an instance
2+
and there is no saved account instance, there will now be a warning detailing
3+
the current filters (``tags``, ``region``, ``plans_preference``) and available instances. If ``plans_preference``
4+
is not set, free and trial plan instances will be prioritized over paid instances.
5+
6+
Additional warnings have also been added to make the current active insance more clear:
7+
8+
- Warning messages to tell users whether they're using a saved account or a new account.
9+
- When retrieving a backend and an instance is automatically selected, there will be a warning with
10+
the instance name and plan type.
11+
12+
It is now also possible to initialize :class:`.QiskitRuntimeService` with just a token. Since the ``ibm_quantum``
13+
channel name has been removed and both the ``ibm_cloud`` and ``ibm_quantum_platform`` channels point to the same
14+
API, the ``channel`` parameter is no longer required. ``ibm_quantum_platform`` is the default channel.

test/integration/test_account.py

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,44 @@ class TestQuantumPlatform(IBMIntegrationTestCase):
7070

7171
def test_initializing_service_no_instance(self):
7272
"""Test initializing without an instance."""
73-
service = QiskitRuntimeService(
74-
token=self.dependencies.token, channel="ibm_quantum_platform", url=self.dependencies.url
75-
)
76-
self.assertTrue(service)
77-
self.assertTrue(service.backends())
73+
74+
# no default instance and no filters
75+
with self.assertLogs("qiskit_ibm_runtime", level="WARNING") as logs:
76+
service = QiskitRuntimeService(
77+
token=self.dependencies.token,
78+
channel="ibm_quantum_platform",
79+
url=self.dependencies.url,
80+
)
81+
self.assertTrue(service)
82+
message = logs.output[1]
83+
self.assertIn("Free and trial", message)
84+
85+
# no defualt instance and plans_preference
86+
with self.assertLogs("qiskit_ibm_runtime", level="WARNING") as logs:
87+
service = QiskitRuntimeService(
88+
token=self.dependencies.token,
89+
channel="ibm_quantum_platform",
90+
url=self.dependencies.url,
91+
plans_preference=["internal"],
92+
)
93+
self.assertTrue(service)
94+
message = logs.output[1]
95+
self.assertNotIn("Free and trial", message)
96+
self.assertIn("available account instances are", message)
97+
98+
# no defualt instance and region
99+
region = "us-east"
100+
with self.assertLogs("qiskit_ibm_runtime", level="WARNING") as logs:
101+
service = QiskitRuntimeService(
102+
token=self.dependencies.token,
103+
channel="ibm_quantum_platform",
104+
url=self.dependencies.url,
105+
region=region,
106+
)
107+
self.assertTrue(service)
108+
message = logs.output[1]
109+
self.assertIn("Free and trial", message)
110+
self.assertIn(f"region: {region}", message)
78111

79112
def test_backends_default_instance(self):
80113
"""Test that default instance returns the correct backends."""

test/unit/mock/fake_runtime_service.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,19 @@ class FakeRuntimeService(QiskitRuntimeService):
4141
{
4242
"crn": "crn:v1:bluemix:public:quantum-computing:my-region:a/...:...::",
4343
"tags": ["services"],
44+
"name": "test-instance",
45+
"pricing_type": "free",
46+
"plan": "internal",
4447
}
4548
]
4649

4750
DEFAULT_INSTANCES = [
4851
{
4952
"crn": "crn:v1:bluemix:public:quantum-computing:my-region:a/...:...::",
5053
"tags": ["services"],
54+
"name": "test-instance",
55+
"pricing_type": "free",
56+
"plan": "internal",
5157
}
5258
]
5359

@@ -110,10 +116,12 @@ def _resolve_cloud_instances(self, instance: Optional[str]) -> List[Tuple[str, L
110116
if not self._backend_instance_groups:
111117
self._backend_instance_groups = [
112118
{
119+
"name": inst["name"],
113120
"crn": inst["crn"],
114121
"plan": inst.get("plan"),
115122
"backends": self._discover_backends_from_instance(inst["crn"]),
116123
"tags": inst.get("tags"),
124+
"pricing_type": inst["pricing_type"],
117125
}
118126
for inst in self._all_instances
119127
]

test/unit/test_account.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -632,17 +632,24 @@ def test_enable_account_by_channel(self):
632632
self.assertEqual(service._account.token, token)
633633

634634
def test_enable_account_by_token_url(self):
635-
"""Test initializing account by token or url."""
635+
"""Test initializing account by token."""
636636
token = uuid.uuid4().hex
637637
subtests = [
638638
{"token": token},
639-
{"url": "some_url"},
640639
{"token": token, "url": "some_url"},
641640
]
642641
for param in subtests:
643642
with self.subTest(param=param):
644-
with self.assertRaises(ValueError):
645-
_ = FakeRuntimeService(**param)
643+
with temporary_account_config_file(channel="ibm_quantum_platform", token=token):
644+
service = FakeRuntimeService(**param)
645+
self.assertTrue(service._account)
646+
647+
def test_enable_account_by_url_error(self):
648+
"""Test initializing account by url gives an error."""
649+
token = uuid.uuid4().hex
650+
with temporary_account_config_file(channel="ibm_quantum_platform", token=token):
651+
with self.assertRaises(ValueError):
652+
_ = FakeRuntimeService(url="some_url")
646653

647654
def test_enable_account_by_name_and_other(self):
648655
"""Test initializing account by name and other."""
@@ -779,12 +786,12 @@ def test_enable_account_by_env_token_url(self):
779786
"QISKIT_IBM_URL": url,
780787
"QISKIT_IBM_INSTANCE": "my_crn",
781788
}
782-
subtests = [{"token": token}, {"url": url}, {"token": token, "url": url}]
789+
subtests = [{"token": token}, {"token": token, "url": url}]
783790
for extra in subtests:
784791
with self.subTest(extra=extra):
785-
with custom_envs(envs) as _, self.assertRaises(ValueError) as err:
786-
_ = FakeRuntimeService(**extra)
787-
self.assertIn("token", str(err.exception))
792+
with custom_envs(envs) as _:
793+
service = FakeRuntimeService(**extra)
794+
self.assertTrue(service._account)
788795

789796
def test_enable_account_bad_name(self):
790797
"""Test initializing account by bad name."""

0 commit comments

Comments
 (0)