Skip to content

Conversation

@daniel-zullo-frequenz
Copy link
Contributor

Move consumer and producer power formulas from logical meter to their own logical components Consumer and Producer, respectively.

Related to #782

@github-actions github-actions bot added part:docs Affects the documentation part:tests Affects the unit, integration and performance (benchmarks) tests part:data-pipeline Affects the data pipeline part:microgrid Affects the interactions with the microgrid labels Nov 29, 2023
@daniel-zullo-frequenz daniel-zullo-frequenz force-pushed the feature/mv-consumer-producer-metrics branch from b49c9f5 to 456e46e Compare November 29, 2023 11:13
Copy link
Contributor

@llucax llucax left a comment

Choose a reason for hiding this comment

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

LGTM. I know the documentation discussion is probably out of scope of this PR, I'd be fine also merging as is and addressing the docs in another PR, although maybe it is worth moving the explanation of what Consumer and Producer is from the method to the class, that shouldn't be too much work.

Comment on lines 18 to 26
"""Calculate high level consumer metrics in a microgrid.
Consumer provides methods for fetching power values from different points
in the microgrid. These methods return `FormulaReceiver` objects, which can
be used like normal `Receiver`s, but can also be composed to form
higher-order formula streams.
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't find this docs very useful. I was trying to come up with a description1 but the concept of consumer is non trivial, so I thought we should probably add a cross-reference to the Glossary, but then I realized the glossary is also outdated or incomplete, because we have only a definition of Consumption ( and Gross Consumption) that refers to the clipped value, and here we actually want to say that this groups everything that is not producing power (in a sense). I notice we are defining a Load in the glossary, and to be honest, IMHO this is the most common name for what we call consumer, is the part of the circuit we don't know about or control.

There is also the wiki glossary that we can take as reference: https://github.com/frequenz-floss/frequenz-sdk-python/wiki/Glossary#microgrid-power-terminology, for example it says:

The term consumer was deliberately chosen to indicate that this asset type is expected to predominantly consume electric power. Under normal circumstances there should be no active sources in this asset type and the metrics consumer_{consumption,production}_power would not be used. Alternative terms: load, demand, sink.

I guess if we don't want to get into the whole load/consumer discussion again, we could just add a Consumer entry in the glossary and that's it.

@frequenz-floss/python-sdk-team @cwasicki FYI

Footnotes

  1. I also wouldn't mention the types returned, like FormulaReceiver, I would explain at a higher level what you can get from this (which for now is just power).

Comment on lines 75 to 86
"""Fetch the consumer power for the microgrid.
Under normal circumstances this is expected to correspond to the gross
consumption of the site excluding active parts and battery.
This formula produces values that are in the Passive Sign Convention (PSC).
If a formula engine to calculate consumer power is not already running,
it will be started.
A receiver from the formula engine can be created using the
`new_receiver` method.
Copy link
Contributor

Choose a reason for hiding this comment

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

OK, I see in here it is more clear, maybe we need to harmonize the docs of the class and property. I think it makes more sense to document in the class what the consumer is.

Comment on lines 18 to 31
"""Calculate high level producer metrics in a microgrid.
Producer provides methods for fetching power values from different points
in the microgrid. These methods return `FormulaReceiver` objects, which can
be used like normal `Receiver`s, but can also be composed to form
higher-order formula streams.
!!! note
`Producer` instances are not meant to be created directly by users.
Use the [`microgrid.producer`][frequenz.sdk.microgrid.producer] method
for creating `Producer` instances.
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comments in general as for Consumer.

@daniel-zullo-frequenz daniel-zullo-frequenz added this to the v1.0.0 milestone Dec 5, 2023
@daniel-zullo-frequenz daniel-zullo-frequenz self-assigned this Dec 5, 2023
@daniel-zullo-frequenz daniel-zullo-frequenz force-pushed the feature/mv-consumer-producer-metrics branch from 456e46e to e7fd44e Compare January 12, 2024 12:01
@github-actions github-actions bot added the part:tooling Affects the development tooling (CI, deployment, dependency management, etc.) label Jan 12, 2024
@daniel-zullo-frequenz daniel-zullo-frequenz force-pushed the feature/mv-consumer-producer-metrics branch 2 times, most recently from bb8b77e to 8292b6b Compare January 12, 2024 14:06
@daniel-zullo-frequenz daniel-zullo-frequenz marked this pull request as ready for review January 12, 2024 14:11
@daniel-zullo-frequenz daniel-zullo-frequenz requested a review from a team as a code owner January 12, 2024 14:11
@daniel-zullo-frequenz daniel-zullo-frequenz force-pushed the feature/mv-consumer-producer-metrics branch from 8292b6b to b35c7d0 Compare January 12, 2024 17:04
llucax
llucax previously approved these changes Jan 15, 2024
Copy link
Contributor

@llucax llucax left a comment

Choose a reason for hiding this comment

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

I did the review mostly by looking at the review comments I made in the past, I didn't do another in-depth review. With this in mind, LGTM!

pyproject.toml Outdated
"google-api-python-client >= 2.71, < 3",
"grpcio >= 1.54.2, < 2",
"grpcio-tools >= 1.54.2, < 2",
"lazy >= 1.6.0, < 2",
Copy link
Contributor

Choose a reason for hiding this comment

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

I was wondering if it was worth adding a dependency for this, as it seems very trivial to implement, but then I looked at it and it is not that simple, the library considers inheritance for example, so I think it is worth having it as a dependency.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alternatively we could use @cached_property

It seems the only difference is a constrain that will be removed in python 3.12:

Changed in version 3.12: Prior to Python 3.12, cached_property included an undocumented lock
to ensure that in multi-threaded usage the getter function was guaranteed to run only once
per instance. However, the lock was per-property, not per-instance, which could result in
unacceptably high lock contention. In Python 3.12+ this locking is removed.

Copy link
Contributor Author

@daniel-zullo-frequenz daniel-zullo-frequenz Jan 26, 2024

Choose a reason for hiding this comment

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

I added a commit to replace @lazy with @cached_property as it seems to be what we needs and it is provided in the standard library. I can just remove the new commit if anyone is against it.

Copy link
Contributor

Choose a reason for hiding this comment

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

I knew about @cached_property but I thought it was deprecated, I must be confusing it with something else. In any case, @cached_property seems like a very different monster, as it allows writes:

The mechanics of cached_property() are somewhat different from property(). A regular property blocks attribute writes unless a setter is defined. In contrast, a cached_property allows writes.

The cached_property decorator only runs on lookups and only when an attribute of the same name doesn’t exist. When it does run, the cached_property writes to the attribute with the same name. Subsequent attribute reads and writes take precedence over the cached_property method and it works like a normal attribute.

I guess this is why all the extra complexity is needed, like locking, etc. It also says the __dict__ will take more space, and all sort of weird implications (which I guess for this case is not that bad).

But just the allowing to write will be problematic for us, as it allows users to overwrite (even if accidentally) the property. Oh, damn, I just did a test, and lazy allows writes too :(

class X:
    @cached_property
    def x(self) -> int:
        return 1

x = X()
print(x.x)
x.x = 10
print(x.x)

class Y:
    @lazy
    def y(self) -> int:
        return 1

y = Y()
print(y.y)
y.y = 10
print(y.y)

Both work and pass mypy without any errors. Not sure what to do now... Maybe we should delay this change and leave it as it was for now. I would also be fine with @cached_property if you prefer. @shsms any opinions from your side?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could write a customize ReadOnlyCachedProperty as follows:

class ReadOnlyCachedProperty(cached_property):
    def __set__(self, *args, **kwargs):
        raise AttributeError("Cannot assign a value to a read-only property")

class X:
    @ReadOnlyCachedProperty
    def x(self) -> int:
        return 1

x = X()
print(x.x)
x.x = 10 # AttributeError: Cannot assign a value to a read-only property

Copy link
Contributor

@shsms shsms Jan 30, 2024

Choose a reason for hiding this comment

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

I don't like the idea of making them writable. They're outside the hot path normally, but I can see the value in caching anyway, because someone might just write really bad code, and that might be very expensive, and unintuitive, because they're just using a property.

The (probably not scalable) approach that I've been following has been to cache in a private global variable and use that.

but just to add one more option into the mix:

class X:
    @property
    def x(self):
        @cache
        def _x():
            # call constructor
        return _x()

or add a from_cache_or_create constructor in underlying class

class SlowConstructable:
    @cache
    @classmethod
    def from_cache_or_create(cls, ...args):
        return cls(...args)

class X:
    @property
    def x(self)
        SlowConstructable.from_cache_or_create(args)

Copy link
Contributor

@llucax llucax Jan 30, 2024

Choose a reason for hiding this comment

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

About Dani's approach, I like that is simple, but I don't like that we'll still use cached_property which is a very complicated beast for something that should be much simpler.

About Sahas' approach I'm not crazy about moving the caching logic to the "upstream" class, if someone wants to cache anything, it should be a downstream problem.

If we really want to go with this, I would either implement the decorator from scratch or something like a LazyValue class so we can do:

class X:
    def __init__(self, ...) -> None:
        self._x = LazyValue(lambda: TheValueClass(...))
    @property
    def x(self):
        return self._x.value

where LazyValue is something like:

class LazyValue[T]:
    def __init__(self, constructor: Callable[[None], T], /) -> None:
        self._value: T | None = None
        self._ctor: Callable[[None], T] = constructor
    @property
    def value(self) -> T:
        if self._value is None:
            self._value = self._ctor()
        return self._value

(we can get away with None here because it would be quite stupid to construct a None lazily... :D)

The decorator approach is definitely the best IMHO in terms of readability and usability though...

But IMHO this went a bit out of hand, it was just a minor suggestion for this PR but totally unrelated. I would just remove the last 3 commits from the PR and merge it, and then decide what to do about this in a separate PR.

Before this PR they were already cached, we just had a lot of duplicate code just checking if the object was already constructed or not and construct it if it weren't (IIRC).

Copy link
Contributor Author

@daniel-zullo-frequenz daniel-zullo-frequenz Jan 30, 2024

Choose a reason for hiding this comment

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

About Dani's approach, I like that is simple, but I don't like that we'll still use cached_property which is a very complicated beast for something that should be much simpler.

I think we could also do it for @lazy

Before this PR they were already cached, we just had a lot of duplicate code just checking if the object was already constructed or not and construct it if it weren't (IIRC).

That's right.

But IMHO this went a bit out of hand, it was just a minor suggestion for this PR but totally unrelated. I would just remove the last 3 commits from the PR and merge it, and then decide what to do about this in a separate PR.

Agree. I think we just need to assess the implications of each solution as the implementation itself should be relatively simple. I'll remove the last 3 commits and create an issue

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed the last 3 commits but kept the slight changes to docstring in _data_pipelines for the "lazy" existent getters in a new commit

@github-actions github-actions bot removed the part:tooling Affects the development tooling (CI, deployment, dependency management, etc.) label Jan 26, 2024
@daniel-zullo-frequenz daniel-zullo-frequenz force-pushed the feature/mv-consumer-producer-metrics branch from 9baa315 to 3f0afdf Compare January 26, 2024 15:35
@llucax llucax modified the milestones: v1.0.0, v1.0.0-rc4 Jan 29, 2024
@daniel-zullo-frequenz daniel-zullo-frequenz force-pushed the feature/mv-consumer-producer-metrics branch from 3f0afdf to ed17fdf Compare January 30, 2024 16:48
The producer and consumer power formulas are now
implemented in their own logical component classes.

Signed-off-by: Daniel Zullo <[email protected]>
Signed-off-by: Daniel Zullo <[email protected]>
Amend data pipeline methods documentation to be more
concise on what it is helpful for the user.

Signed-off-by: Daniel Zullo <[email protected]>
@daniel-zullo-frequenz daniel-zullo-frequenz added this pull request to the merge queue Jan 31, 2024
Merged via the queue into frequenz-floss:v1.x.x with commit 7161472 Jan 31, 2024
@daniel-zullo-frequenz daniel-zullo-frequenz deleted the feature/mv-consumer-producer-metrics branch January 31, 2024 16:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

part:data-pipeline Affects the data pipeline part:docs Affects the documentation part:microgrid Affects the interactions with the microgrid part:tests Affects the unit, integration and performance (benchmarks) tests

Projects

Development

Successfully merging this pull request may close these issues.

3 participants