-
Notifications
You must be signed in to change notification settings - Fork 17
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
Parameters builder #1123
Conversation
Codecov ReportAttention: Patch coverage is
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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
There was a problem hiding this 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/parameters.py
Outdated
InstrumentMap = dict[InstrumentId, Instrument] | ||
|
||
|
||
class Hardware(TypedDict): |
There was a problem hiding this comment.
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
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: |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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/parameters.py
Outdated
if natives is None: | ||
natives = set() | ||
else: | ||
natives = set(natives) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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).
There was a problem hiding this comment.
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
- still,
- 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 sufficientSuggested changeif 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
There was a problem hiding this comment.
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.
@stavros11 to me, this was already good to go when I reviewed it. Feel free to merge whenever you wish :) |
Implements the
ParametersBuilder
discussed with @alecandido last week. The main change, from a usability point of view, is thatPlatform
s can now be defined without writing the correspondingparameters.json
. In that case, a default parameter configuration will be autogenerated, using the newParametersBuilder
object.The generated
Parameters
will have configs with zero values for all channels defined within thecreate_method
. They will also have default native gates (also with zeros) for all gates specified asnatives
in theParametersBuilder
. Note thatParametersBuilder
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 usingParameter.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.).