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

Add pv optimization tutorial #700

Open
wants to merge 1 commit into
base: v1.x.x
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions docs/tutorials/pv_optimization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Optimizing PV production

## Introduction

Before we start it's assumed that you have finished the first [tutorial](./getting_started.md)

In this tutorial you will write an application that optimizes the energy produced from a
[PV](intro/glossary/#pv) system with Battery for self consumption.
In order to do so you need to measure the power that flows through the
[grid connection point](../../intro/glossary/#grid) to determine excess power.
Copy link
Contributor

Choose a reason for hiding this comment

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

Once #722 is merged, we'll no longer have to type paths and slugs directly:

Suggested change
[grid connection point](../../intro/glossary/#grid) to determine excess power.
{{glossary("Grid", "grid connection point")}} to determine excess power.

Copy link
Contributor

Choose a reason for hiding this comment

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

also the dir is not called "intro" anymore, it got renamed to "user-guide".

Copy link
Contributor

Choose a reason for hiding this comment

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

#722 is merged btw, so this is available upstream.


## Measure the excess power

When using the term excess power what we actually mean is the consumer excess power, that is the power that
Copy link
Contributor

Choose a reason for hiding this comment

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

quotes would be good around the term here

flows from the PV system into the grid.

!!! note

We are using the [passive sign convention](../../intro/glossary/#passive-sign-convention) and thus power
flowing from PV is negative and consumed power is positive.

We want to measure the excess power. In order to do so you can use the SDK's data pipeline and especially
the pre defined
[consumer](../../reference/frequenz/sdk/timeseries/logical_meter/#frequenz.sdk.timeseries.logical_meter.LogicalMeter.consumer_power)
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be

Suggested change
[consumer](../../reference/frequenz/sdk/timeseries/logical_meter/#frequenz.sdk.timeseries.logical_meter.LogicalMeter.consumer_power)
[consumer][frequenz.sdk.timeseries.logical_meter.LogicalMeter.consumer_power]

and
[producer power](../../reference/frequenz/sdk/timeseries/logical_meter/#frequenz.sdk.timeseries.logical_meter.LogicalMeter.producer_power)
formulas.

```python
async def run() -> None:
... # (1)!

# negative means feed-in power due to the negative sign convention
consumer_excess_power_engine = (
microgrid.logical_meter().producer_power
+ microgrid.logical_meter().consumer_power
).build("excess_power") # (2)!
cons_excess_power_recv = consumer_excess_power_engine.new_receiver() # (3)!
```

1. The initialization code as explained in the Getting Started tutorial.
2. Construct the consumer excess power by summing up consumer and producer power each of which having
opposite signs due to the sign convention. This returns a formula engine.
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be nice to link to FormulaEngine here, but, it is not public :-/

I think all classes that are returned should be public, because otherwise the user doesn't know what they have to offer, even when looking at the API docs, for example:

https://frequenz-floss.github.io/frequenz-sdk-python/v0.25/reference/frequenz/sdk/timeseries/logical_meter/#frequenz.sdk.timeseries.logical_meter.LogicalMeter.consumer_power

image

(no link in the return type, so the user don't know what can be done with it)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah this is silly

3. Request a receiver from the formula engine which will be used to consume the stream.

## Control the Battery

Now, with the constructed excess power data stream use a
[battery pool](../../reference/frequenz/sdk/timeseries/battery_pool/) to implement control logic.

```python
...

battery_pool = microgrid.battery_pool() # (1)!

async for cons_excess_power in cons_excess_power_recv: # (2)!
cons_excess_power = cons_excess_power.value # (3)!
if cons_excess_power is None: # (4)!
continue
if cons_excess_power <= Power.zero(): # (5)!
await battery_pool.charge(-cons_excess_power)
elif cons_excess_power > Power.zero():
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: the condition here is redundant, it can just be a simple else, but maybe you want to make it explicit for educational purposes.

You could also use propose_power(), right? I guess you are not using it also for educational purposes?

Copy link
Contributor

Choose a reason for hiding this comment

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

oh yes, the charge/discharge methods are no longer available, need to be replaced by the new propose_* ones.

Copy link
Contributor

Choose a reason for hiding this comment

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

We should ideally enable examples testing for docs/ too. @Marenz do you think it would be simple to implement?

Copy link
Contributor

Choose a reason for hiding this comment

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

I believe it isn't hard, but it will probably need some reading up on how exactly do do it, maybe half a days work?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it might be worth it now that we have more examples in docs/.

I would also like to resurrect the idea about just extracting examples to separate files and run all the tools as normal on them. Another advantage of this is we need to run pytest in all combinations of python and archs, but linting could be done only once, there is no need to run the linting on all python versions and archs, and in particular testing examples is super slow, so it makes test in arm64 take much longer than needed.

But the later is probably a fair amount of extra work, so we can also do it as a second step and first only focus on getting docs/ tested.

Copy link
Contributor

@llucax llucax Nov 6, 2023

Choose a reason for hiding this comment

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

await battery_pool.discharge(cons_excess_power)
```

1. Get an instance of the battery pool.
2. Iterate asynchronously over the constructed consumer excess power stream.
3. Get the `Quantity` from the received `Sample`.
4. Do nothing if we didn't receive new data.
5. Charge the battery if there is excess power and discharge otherwise.

And that's all you need to know to write the simple application for storing PV excess power in a battery.

## Full example

Here is a copy & paste friendly version

```python
import asyncio

from datetime import timedelta
from frequenz.sdk import microgrid
from frequenz.sdk.actor import ResamplerConfig
from frequenz.sdk.timeseries import Power

async def run() -> None:
# This points to the default Frequenz microgrid sandbox
microgrid_host = "microgrid.sandbox.api.frequenz.io"
microgrid_port = 62060

# Initialize the microgrid
await microgrid.initialize(
microgrid_host,
microgrid_port,
ResamplerConfig(resampling_period=timedelta(seconds=1)),
)

# negative means feed-in power due to the negative sign convention
consumer_excess_power_engine = (
microgrid.logical_meter().producer_power
+ microgrid.logical_meter().consumer_power
).build("excess_power") # (2)!
cons_excess_power_recv = consumer_excess_power_engine.new_receiver() # (3)!

battery_pool = microgrid.battery_pool() # (1)!
Copy link
Contributor

Choose a reason for hiding this comment

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

do the numbered comments make sense when there are no points below the block? does it reuse the numbered comments from previous examples?


async for cons_excess_power in cons_excess_power_recv: # (2)!
cons_excess_power = cons_excess_power.value # (3)!
if cons_excess_power is None: # (4)!
continue
if cons_excess_power <= Power.zero(): # (5)!
await battery_pool.charge(-cons_excess_power)
elif cons_excess_power > Power.zero():
await battery_pool.discharge(discharge_power)

def main() -> None:
asyncio.run(run())

if __name__ == "__main__":
main()
```

## Further reading

To create more advanced applications it is suggested to read the [actors](../../intro/actors/) documentation.
Loading