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

Merged
merged 12 commits into from
Jan 16, 2025
Merged

Parameters builder #1123

merged 12 commits into from
Jan 16, 2025

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 93.15068% with 5 lines in your changes missing coverage. Please review.

Project coverage is 51.44%. Comparing base (cbaf2c6) to head (3af2909).
Report is 13 commits behind head on main.

Files with missing lines Patch % Lines
src/qibolab/_core/parameters.py 93.54% 4 Missing ⚠️
src/qibolab/_core/platform/load.py 87.50% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1123      +/-   ##
==========================================
+ Coverage   50.65%   51.44%   +0.79%     
==========================================
  Files          63       63              
  Lines        2922     2978      +56     
==========================================
+ Hits         1480     1532      +52     
- Misses       1442     1446       +4     
Flag Coverage Δ
unittests 51.44% <93.15%> (+0.79%) ⬆️

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.

src/qibolab/_core/platform/load.py Outdated Show resolved Hide resolved
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.

src/qibolab/_core/parameters.py Outdated Show resolved Hide resolved
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).

Copy link
Member Author

@stavros11 stavros11 Dec 20, 2024

Choose a reason for hiding this comment

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

Changed to function in 0375df8, just because it is slightly simpler, even though I am also not against the builder, as it gives us runtime type checking and some casting for free.

or even a constructor of Parameters (but maybe I'd avoid this last option, to keep it more modular).

Out of curiosity, if we are going with function, what is the problem with the constructor? From the user side, it seems easier to remember:

from qibolab import Parameters

parameters = Parameters.initialize(...)

than

from qibolab import initialize_parameters

parameters = initialize_parameters(...)

It is just that in the first case we have to make Parameters public, as currently it is not, however we have to make the initializer public anyway even in the second case.

Copy link
Member

Choose a reason for hiding this comment

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

To be fair, we could even make it a constructor, for the time being. But I consider classes in Python not very scalable, mostly because they are bound to have all definitions in a single block (which has also to live in a single module), and after that we should start defining functions, at least internally, to then alias them with class' methods.

In general, the classmethod and staticmethod as well are not a bad idea, since they allow placing some functions in a certain scope. But you're not completely free to play with scopes, since you're constrained by the single block.

In general, I'm trying to split data and their manipulation, to isolate more the operations. Thus, I'm pushing for dataclasses (and similar) to hold the information, but often with little to no methods - using functions to implement the operations.

With "isolating" I mean limiting the data access, and limit even more mutation, whenever possible. Which is a bit against how classes are often used.
While I consider this an improvement, I'm not at all against every aspect of Python classes. Generating scopes is a good one - just limited.

src/qibolab/_core/platform/platform.py Outdated Show resolved Hide resolved
@stavros11 stavros11 requested a review from alecandido January 8, 2025 09:32
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.

Some very minor comments (aesthetics & future reminders).

It already seems good to me. Thanks!

src/qibolab/_core/platform/load.py Show resolved Hide resolved
Comment on lines 310 to 313
if natives is None:
natives = set()
else:
natives = set(natives)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if natives is None:
natives = set()
else:
natives = set(natives)
natives = set(natives if natives is not None else ())

the usual ternary statement, since the is assignment is not conditional (only the value is conditioned).

Copy link
Member

Choose a reason for hiding this comment

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

In principle:

  • you could even make it even simpler, since () (empty tuple) is immutable, and thus an acceptable value for a default
    • still, None is a more natural default for an intentionally empty field
  • given that the type hint is already Optional[set[str]], if that's reliable, a set is already expected for a non-empty value - therefore, the following should be sufficient
    Suggested change
    if natives is None:
    natives = set()
    else:
    natives = set(natives)
    natives = natives if natives is not None else set()
    • that would be consistent, but if you want to be more lenient (that's not required - but it may be convenient if you already have a use case), I'd reflect that in the type hint

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed in fcbfddd, thanks.

Ideally, the type hint should be followed, however, if I remember correctly, this is a user facing function and since type hints are not strictly enforced in Python, we cannot be 100% certain it is reliable, that's why I think it is safer to cast to set internally. A way to enforce this was to convert the arguments to a Model class (the initial approach) and have pydantic enforce the type hints, but as discussed before this is not strictly required here.

Also, indeed, using empty tuples instead of None for this and also the pairs argument, could be another acceptable option. I left None as more natural.

src/qibolab/_core/parameters.py Outdated Show resolved Hide resolved
@alecandido
Copy link
Member

@stavros11 to me, this was already good to go when I reviewed it. Feel free to merge whenever you wish :)

@stavros11 stavros11 merged commit b873571 into main Jan 16, 2025
28 checks passed
@stavros11 stavros11 deleted the parameters-builder branch January 16, 2025 08:36
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