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

Parameters builder #1123

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open

Parameters builder #1123

wants to merge 4 commits into from

Conversation

stavros11
Copy link
Member

Implements the ParametersBuilder discussed with @alecandido last week. The main change, from a usability point of view, is that Platforms can now be defined without writing the corresponding parameters.json. In that case, a default parameter configuration will be autogenerated, using the new ParametersBuilder object.

The generated Parameters will have configs with zero values for all channels defined within the create_method. They will also have default native gates (also with zeros) for all gates specified as natives in the ParametersBuilder. Note that ParametersBuilder makes some assumptions about the config kinds and channels that native gates play on, which the user may need to change in practice for things to work properly on hardware. The updates can be done either directly in Python using Parameter.replace API or dumping to JSON and updating manually.

@alecandido let me know if this is what you also had in mind, because I am not sure. Then we can add some examples in the documentation of how this can be used to simplify platform creation (avoid copy-pasting, etc.).

@stavros11 stavros11 requested a review from alecandido December 17, 2024 12:53
Copy link

codecov bot commented Dec 17, 2024

Codecov Report

Attention: Patch coverage is 89.28571% with 9 lines in your changes missing coverage. Please review.

Project coverage is 51.53%. Comparing base (cbaf2c6) to head (8725616).

Files with missing lines Patch % Lines
src/qibolab/_core/parameters.py 94.02% 4 Missing ⚠️
src/qibolab/_core/platform/load.py 66.66% 3 Missing ⚠️
src/qibolab/_core/platform/platform.py 75.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1123      +/-   ##
==========================================
+ Coverage   50.65%   51.53%   +0.88%     
==========================================
  Files          63       63              
  Lines        2922     2994      +72     
==========================================
+ Hits         1480     1543      +63     
- Misses       1442     1451       +9     
Flag Coverage Δ
unittests 51.53% <89.28%> (+0.88%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Member

@alecandido alecandido left a comment

Choose a reason for hiding this comment

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

Minor considerations, but that's mostly what I had in mind.

In particular, the builder implementation (i.e. the content of .build() and the functions it is calling) is especially useful to initialize a platform from scratch.

I will try to use this myself (I have a use case with the testing I'll do for Qblox), and keep it as the core of a tutorial I may write later on.

All in all, let's refine the implementation, but concerning the substance, we could even merge as it is.

Comment on lines +86 to +89
if parameters is None:
return Platform.load(path, **hardware)

return Platform(**hardware, parameters=parameters)
Copy link
Member

Choose a reason for hiding this comment

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

This part I'm not completely sure about... maybe I'd limit to

Suggested change
if parameters is None:
return Platform.load(path, **hardware)
return Platform(**hardware, parameters=parameters)
return Platform.load(path, **hardware)

and avoid parameters as input.

My idea is that create_platform is often the function used by the backend and high-level users (they are calling it by name).
Instead, for platforms' creators, we may just expose the _load() function (dropping the _, of course), such that you can create it using Platform() yourself.

The main benefit would be to keep the two functions simpler and modular (otherwise create_platform() would cater for three distinct situations, and essentially wrap _load() when you need just that).

InstrumentMap = dict[InstrumentId, Instrument]


class Hardware(TypedDict):
Copy link
Member

Choose a reason for hiding this comment

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

I'm not fully convinced with this name. Though I used it in the first place...

But I currently have no better proposal.

InstrumentMap = dict[InstrumentId, Instrument]


class Hardware(TypedDict):
Copy link
Member

Choose a reason for hiding this comment

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

The TypedDict is perfect to annotate **kwargs, or other places where you're bound to use dictionaries anyhow.

However, the moment it gets part of a serialized hierarchy (cf. ParametersBuilder below), it may be worth to make it a full-fledged Model.

Comment on lines +299 to +307
class ParametersBuilder(Model):
"""Generates default ``Parameters`` for a given platform hardware
configuration."""

hardware: Hardware
natives: set[str] = Field(default_factory=set)
pairs: list[str] = Field(default_factory=list)

def build(self) -> Parameters:
Copy link
Member

Choose a reason for hiding this comment

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

Making it a builder was just instinctive, having in mind something like this.

However, while the builder pattern may be useful to break down a constructor of something complex (essentially, currying it), in this case it is sufficiently simple that we could keep it as a standalone function, or even a constructor of Parameters (but maybe I'd avoid this last option, to keep it more modular).

The equivalent function would read sufficiently smooth anyhow:

def init_parameters(hardware: Hardware, natives: Optional[set[str]] = None, pairs: Optional[list[str]] = None):
    ...

In any case, I'm not strongly against the builder as well.
It was just to acknowledge that, reading the final result, in the end you were right, and it seems unnecessary (but not necessarily bad).

Comment on lines +314 to +320
hardware = {"instruments": instruments, "qubits": qubits, "couplers": couplers}
try:
parameters = Parameters.model_validate_json((path / PARAMETERS).read_text())
except FileNotFoundError:
parameters = ParametersBuilder(hardware=hardware).build()

return cls(name=name, parameters=parameters, **hardware)
Copy link
Member

Choose a reason for hiding this comment

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

In this case, maybe I'm plainly against. The code is still simple enough, so that's not a big deal, but the load function would then attempt doing two different things, which may complicate (slightly, but significantly) the workflow of the code based on it.

Instead, I would keep Platform.load() with the former behavior, and leave the responsibility to the user to handle the missing parameters file case. Which should only happen during platform's creation, and it's sufficiently simple anyhow, i.e.

params = ParametersBuilder(hardware=hardware).build()
Platform(instruments=instruments, qubits=qubits, couplers=couplers, parameters=params)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants