diff --git a/documentation/api/introduction.rst b/documentation/api/introduction.rst index d802adedb..df8fa2258 100644 --- a/documentation/api/introduction.rst +++ b/documentation/api/introduction.rst @@ -257,7 +257,7 @@ In case of a single group of connections, the message may be flattened to: Timeseries ^^^^^^^^^^ -Timestamps and durations are consistent with the ISO 8601 standard. All timestamps in requests to the API must be timezone aware. The timezone indication "Z" indicates a zero offset from UTC. Additionally, we use the following shorthand for sequential values within a time interval: +Timestamps and durations are consistent with the ISO 8601 standard. All timestamps in requests to the API must be timezone-aware. The timezone indication "Z" indicates a zero offset from UTC. Additionally, we use the following shorthand for sequential values within a time interval: .. code-block:: json diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 52a2e033e..07c8d3c19 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -22,6 +22,7 @@ Bugfixes Infrastructure / Support ---------------------- +* Added concept pages to documentation [see `PR #65 `_] * Dump and restore postgres database as CLI commands [see `PR #68 `_] * Improved installation tutorial as part of [`PR #54 `_] * Moved developer docs from Readmes into the main documentation [see `PR #73 `_] @@ -29,6 +30,7 @@ Infrastructure / Support + v0.2.3 | February 27, 2021 =========================== diff --git a/documentation/qa/algorithms.rst b/documentation/concepts/algorithms.rst similarity index 50% rename from documentation/qa/algorithms.rst rename to documentation/concepts/algorithms.rst index 77ed47408..aaadf1c5f 100644 --- a/documentation/qa/algorithms.rst +++ b/documentation/concepts/algorithms.rst @@ -1,19 +1,29 @@ .. _algorithms: -What algorithms does the platform use? + +Algorithms ========================================== +.. contents:: + :local: + :depth: 2 + Forecasting ----------- Forecasting algorithms are used by FlexMeasures to assess the likelihood of future consumption/production and prices. -Weather forecasting is included in the platform, but is not the result of an internal algorithm (see :ref:`weather`). +Weather forecasting is included in the platform, but is usually not the result of an internal algorithm (weather forecast services are being used by import scripts, e.g. with `this tool `_). + +FlexMeasures uses linear regression and falls back to naive forecasting of the last known value if errors happen. +What might be even more important than the type of algorithm is the features handed to the model ― lagged values (e.g. value of the same time yesterday) and regressors (e.g. wind speed prediction to forecast wind power production). + + The performance of our algorithms is indicated by the mean absolute error (MAE) and the weighted absolute percentage error (WAPE). Power profiles on an asset level often include zero values, such that the mean absolute percentage error (MAPE), a common statistical measure of forecasting accuracy, is undefined. For such profiles, it is more useful to report the WAPE, which is also known as the volume weighted MAPE. The MAE of a power profile gives an indication of the size of the uncertainty in consumption and production. -This allows the user to compare an asset's predictability to its flexibility, i.e. to the size of possible flexibility actions. +This allows the user to compare an asset's predictability to its flexibility, i.e. to the size of possible flexibility activations. Example benchmarks per asset type are listed in the table below for various assets and forecasting horizons. FlexMeasures updates the benchmarks automatically for the data currently selected by the user. @@ -54,44 +64,70 @@ Defaults: - Lagged outcome variables are selected based on the periodicity of the asset (e.g. daily and/or weekly). - Common external variables are weather forecasts of temperature, wind speed and irradiation. - Timeseries data with frequent zero values are transformed using a customised Box-Cox transformation. -- To avoid overfitting, cross-validation is used. -- Before fitting, explicit annotations of expert knowledge to the model (like the definition of asset-specific seasonalities and special time events) are possible. +- To avoid over-fitting, cross-validation is used. +- Before fitting, explicit annotations of expert knowledge to the model (like the definition of asset-specific seasonality and special time events) are possible. - The model is currently fit each day for each asset and for each horizon. Improvements: -- Most assets have yearly seasonalities (e.g. wind, solar) and therefore forecasts would benefit from >= 2 years of history. +- Most assets have yearly seasonality (e.g. wind, solar) and therefore forecasts would benefit from >= 2 years of history. -Broker ------- +Scheduling +------------ -A broker algorithm is used by the Aggregator to analyse flexibility in the Supplier's portfolio of assets, and to suggest the most valuable DR actions to take for each time slot. -The actions are presented to the Supplier as flexibility offers in the form of an order book. +Given price conditions or other conditions of relevance, a scheduling algorithm is used by the Aggregator (in case of explicit DR) or by the Energy Service Company (in case of implicit DR) to form a recommended schedule for the Prosumer's flexible assets. -Defaults: -- +Storage devices +^^^^^^^^^^^^^^^ -Trading -------- +So far, FlexMeasures provides algorithms for storage ― for batteries (e.g. home batteries or EVs) and car charging stations. +We thus cover the asset types "battery", "one-way_evse" and "two-way_evse". -A trading algorithm is used to assist the Supplier with its decision-making across time slots, based on the order books made by the broker (see above). -The algorithm suggests which offers should be accepted next, and the Supplier may automate its decision-making by letting the algorithm place orders on its behalf. +These algorithms schedule the storage assets based directly on the latest beliefs regarding market prices, within the specified time window. +They are mixed integer linear programs, which are configured in FlexMeasures and then handed to a dedicated solver. -Defaults: +For all scheduling algorithms, a starting state of charge (SOC) as well as a set of SOC targets can be given. If no SOC is available, we set the starting SOC to 0. -- (Myopic greedy strategy) Order all flexibility with a positive expected value in the first available timeslot, then those in the second available timeslot, and so on. +Also, per default we incentivise the algorithms to prefer scheduling charging now rather than later, and discharging later rather than now. +We achieve this by adding a tiny artificial price slope. We penalise the future with at most 1 per thousand times the price spread. This behaviour can be turned off with the `prefer_charging_sooner` parameter set to `False`. +.. note:: For the resulting consumption schedule, consumption is defined as positive values. + +Possible future work on algorithms +----------------------------------- +Enabling more algorithmic expression in FlexMeasures is crucial. This are a few ideas for future work. Some of them are excellent topics for Bachelor or Master theses. so get in touch if that is of interest to you. -Planning --------- +More configurable forecasting +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +On the roadmap for FlexMeasures is to make features easier to configure, especially regressors. +Furthermore, we plan to add more types of forecasting algorithms, like random forest or even LSTM. -Based on decisions about control actions, a planning algorithm is used by the Aggregator (in case of explicit DR) or by the Energy Service Company (in case of implicit DR) -to form instructions for the Prosumer's flexible assets. -Defaults: +Other optimisation goals for scheduling +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Next to market prices, optimisation goals like reduced CO₂ emissions are sometimes required. There are multiple ways to measure this, e.g. against the CO₂ mix in the grid, or the use of fossil fuels. + + +Scheduling of other flexible asset types +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Next to storage, there are other interesting flexible assets which can require specific implementations. +For shifting, there are heat pumps and other buffers. For curtailment, there are wind turbines and solar panels. + +.. note:: See :ref:`flexibility_types` for more info on shifting and curtailment. + +Broker algorithm +^^^^^^^^^^^^^^^^^ +A broker algorithm is used by the Aggregator to analyse flexibility in the Supplier's portfolio of assets, and to suggest the most valuable flexibility activations to take for each time slot. +The differences to single-asset scheduling are that these activations are based on a helicopter perspective (the Aggregator optimises a portfolio, not a single asset) and that the flexibility offers are presented to the Supplier in the form of an order book. + + +Trading algorithm +^^^^^^^^^^^^^^^^^^ +A trading algorithm is used to assist the Supplier with its decision-making across time slots, based on the order books made by the broker (see above). +The algorithm suggests which offers should be accepted next, and the Supplier may automate its decision-making by letting the algorithm place orders on its behalf. -- \ No newline at end of file +A default approach would be a myopic greedy strategy ― order all flexibility opportunities with a positive expected value in the first available timeslot, then those in the second available timeslot, and so on. diff --git a/documentation/qa/assets.rst b/documentation/concepts/assets.rst similarity index 100% rename from documentation/qa/assets.rst rename to documentation/concepts/assets.rst diff --git a/documentation/benefits.rst b/documentation/concepts/benefits.rst similarity index 51% rename from documentation/benefits.rst rename to documentation/concepts/benefits.rst index 14eaca964..25085708c 100644 --- a/documentation/benefits.rst +++ b/documentation/concepts/benefits.rst @@ -3,34 +3,35 @@ .. _benefits: ************************************************** -Benefits of the FlexMeasures Platform +Benefits ************************************************** Automation ------------- -FlexMeasures provides decision-making support so that the platform operator can schedule flexibility actions. -It forecasts the state of assets and proposes the best flexibility actions (shifting or curtailment) -for future periods that need actions. This is done with modern forecasting and scheduling intelligence. +FlexMeasures provides decision-making support so that the platform operator can schedule flexibility activations. +It forecasts the state of assets and proposes the best flexibility activations (shifting or curtailment) +for future periods. This is done with modern forecasting and scheduling intelligence. Insight -------------- Both platform operator and asset owners can monitor the assets - past and current states as well as forecasts are displayed numerically in plots and tables. -Flexibility actions ordered in the past can be reviewed. -Proposed and scheduled flexibility actions show their expected effects (on imbalance as well as on financial returns). +Activations of flexibility which were ordered in the past can be reviewed. +Proposed and scheduled flexibility activations show their expected effects (on imbalance as well as on financial returns). Autonomy -------------- The companies connected to FlexMeasures only give up as much control as necessary. The asset owners still control the main behaviour of their assets. -The owners allow the platform operator to schedule flexibility actions within limits they can set. +The owners allow the platform operator to schedule flexibility activations within limits they can set. Also the platform operator stays in charge: -They can choose to approve all proposed flexibility actions manually or to let FlexMeasures automatically schedule them. +They can choose to approve all proposed flexibility activations manually or to let FlexMeasures automatically schedule them. As FlexMeasures is open source, they can choose to host it themselves or let a third party (like Seita BV) do that. Profit sharing --------------- -The platform operator and asset owners share the profit made from flexibility actions between them. -FlexMeasures provides basic accounting. +The platform operator (as ESCo or Aggregator) and asset owners can share the profit made from flexibility activations between them. +FlexMeasures plans on providing basic accounting for this. +.. note:: Read more on flexibility opportunities and activations, as well as profit sharing on :ref:`benefits_of_flex` diff --git a/documentation/concepts/benefits_of_flex.rst b/documentation/concepts/benefits_of_flex.rst new file mode 100644 index 000000000..aadc75af2 --- /dev/null +++ b/documentation/concepts/benefits_of_flex.rst @@ -0,0 +1,143 @@ +.. _benefits_of_flex: + +Benefits from energy flexibility +==================================== + +FlexMeasures was created so that the value of energy flexibility can be realised. +This will make energy cheaper to use, and can also reduce CO₂ emissions. +Here, we define a few terms around this idea, which come up in other parts of this documentation. + +.. contents:: + :local: + :depth: 2 + + +Flexibility opportunities and activation +----------------------------------------- + +Opportunities +^^^^^^^^^^^^^^ + +In an energy system with flexible energy assets present (e.g. batteries, heating/cooling), there are +opportunities to profit from the availability and activation of their flexibility. + +Energy flexibility can come from the ability to store energy ("storage"), to delay (or advance) planned consumption or production ("shifting"), the ability to lower production ("curtailment"), or the ability to increase or decrease consumption ("demand response") ― see :ref:`flexibility_types` for a deeper discussion. + +Under a given incentive, this flexibility represents an opportunity to profit by scheduling consumption or production differently than originally planned. +Within FlexMeasures, flexibility is represented as the difference between a suggested schedule and a given baseline. +By default, a baseline is determined by our own forecasts. + +Opportunities are expressed with respect to given economical and ecological incentives. +For example, a suggested schedule may represent an opportunity to save X EUR and Y tonnes of CO₂. + +Activation +^^^^^^^^^^^^^^^ + +The activation of flexibility usually happens in a context of incentives. Often, that context is a market. +We recommend `the USEF white paper on the flexibility value chain `_ for an excellent introduction of who can benefit from energy flexibility and how it can be delivered. +The high-level takeaways are these: + +- the value of flexibility flows back to Prosumers along a chain of roles involved in the activation of their flexibility: the **Flexibility Value Chain**. +- a portfolio of flexible assets (and even individual assets) may provide services in multiple contexts in the same period: **value stacking**. +- **Explicit demand-side flexibility** services involve Aggregators, while **implicit demand-side flexibility** services involve Energy Service Companies (ESCos). +- Many remuneration components for flexibility services requires the determination of a baseline according to some **baseline methodology**. +- Both availability and activation of flexibility have value. + +The overall value (from availability and activation of flexibility), and how this value is shared amongst stakeholders in the various roles in the Flexibility Value Chain, can be accounted for by the platform operator. +We talk more about this in :ref:`activation_profits`. + + +An example: the balancing market +---------------------------------------- +An example of a market on which flexibility can be activated is the balancing market, which is meant to bring the grid frequency back to a target level within a matter of minutes. +Consider the aforementioned differences between suggested schedules and a given baseline. +In the context of the balancing market, differences indicating an increase in production or a decrease in consumption on activation both result in an increasing grid frequency (back towards the target frequency). + +The balancing market pays for such services, and they are often referred to as `"up-regulation"`. +It works the other way around, too: differences indicating a decrease in production or an increase in consumption both result in a decreasing grid frequency (`"down-regulation"`). + + +.. _flexibility_types: + +Types of flexibility +-------------------------------------- + +The FlexMeasures platform distinguishes between different types of flexibility. We explain them here in more detail, together with examples. + + +Curtailment +^^^^^^^^^^^^^^ + +Curtailment happens when an asset temporarily lowers or stops its production or consumption. +A defining feature of curtailment is that total production or consumption decreases when this this flexibility is activated. + +- A typical example of curtailing production is when a wind turbine adjusts the pitch angle of its blades to decrease the generator torque. +- An example of curtailing consumption is load shedding of energy intensive industries. + +Curtailment offers may specify some freedom in terms of how much energy can be curtailed. +In these cases, the user can select the energy volume (in MWh) to be ordered, within constraints set by the relevant Prosumer. +The net effect of a curtailment action is also measured in terms of an energy volume (see the flexibility metrics in the :ref:`portfolio` page). + +Note that the volume ordered is not necessarily equal to the volume curtailed: +the ordered volume relates only to the selected time window, while the curtailed volume may include volumes outside of the selected time window. +For example, an asset that runs an all-or-nothing consumption process of 2 hours can be ordered to curtail consumption for 1 hour, but will in effect stop the entire process. +In this case, the curtailed volume will be higher than the ordered volume, and the platform will take into account the total expected curtailment in its calculations. + +Shifting +^^^^^^^^^^^^^^ + +Shifting happens when an asset delays or advances its energy production or consumption. +A defining feature of shifting is that total production or consumption remains the same when this flexibility is activated. + +- An example of delaying consumption is when a charging station postpones the charging process of an electric vehicle. +- An example of advancing consumption is when a cooling unit starts to cool before the upper temperature bound was reached (pre-cooling). + +Shifting offers may specify some freedom in terms of how much energy can be shifted. +In these cases, the user can select the energy volume (in MWh) to be ordered, within constraints set by the relevant Prosumer. +This energy volume represents how much energy is shifting into or out of the selected time window. +The net effect of a shifting action is measured in terms of an energy-time volume (see the flexibility metrics in the :ref:`portfolio` page). +This volume is a multiplication of the energy volume being shifted and the duration of that shift. + + +.. _activation_profits: + +Profits of flexibility activation +--------------- + +The realised value from activating flexibility has to be computed and accounted for. +Both of these activities depend on the context in which FlexMeasures is being used, and we expect that it will often have to be implemented in a custom manner (much as the actual scheduling optimisation). + +.. todo:: Making it possible to configure custom scheduling and value accounting is on the roadmap for FlexMeasures. + +Computing value +^^^^^^^^^^^^^^^^ + +The computation of the value is what drives the scheduling optimisation. +This value is usually monetary, and in that case there should be some form of market configured. +This can be a constant or time-of-use tariff, or a real market. +However, there are other possibilities, for instance if the optimisation goal is to minimise CO₂ emissions. +Then, the realised value is avoided CO₂, which nowadays has an assumed value, e.g. in `the EU ETS carbon market `_. + + +Accounting / Sharing value +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The realisation of payments is outside of the scope of FlexMeasures, but it can provide the accounting to enable them (as was said above, this is usually a part of the optimisation problem formulation). + +However, next to fuelling algorithmic optimisation, the way that the value of energy flexibility is shared among the stakeholders will also be an important driver for project participation. Accounting plays an important role here. + +There are different roles in a modern smart energy system (e.g. "Prosumer", "DSO", Aggregator", "ESCo"), +and they all enjoy the benefits of flexibility in different ways +(see for example `this resource `_ for more details). + +In our opinion, the only way to successful implementation of energy flexibility is if profits +are shared between these stakeholders. This assumes contractual relationships. Use cases which FlexMeasures +can support well are the following relationships: + +* between Aggregator and Prosumer, where the Aggregator sells the balancing power to a third party and shares the profits with the Prosumer according to some contracted method for profit sharing. In this case the stated costs and revenues for the Prosumer may be after deducting the Aggregator fee (which typically include price components per flex activation and price components per unit of time, but may include arbitrarily complex price components). + +* between ESCo and Prosumer, where the ESCo advises the Prosumer to optimise against e.g. dynamic prices. Likewise, stated numbers may be after deducting the ESCo fee. + +FlexMeasures can take these intricacies into account if a custom optimisation algorithm is plugged in to model them. + +Alternatively, we can assume that all profit from activating flexibility goes to the Prosumer, or simply report the profits before sharing (and before deducting any service fees). diff --git a/documentation/qa/markets.rst b/documentation/concepts/markets.rst similarity index 100% rename from documentation/qa/markets.rst rename to documentation/concepts/markets.rst diff --git a/documentation/qa/data.rst b/documentation/concepts/security_auth.rst similarity index 95% rename from documentation/qa/data.rst rename to documentation/concepts/security_auth.rst index 765e4e7ff..98eaa6465 100644 --- a/documentation/qa/data.rst +++ b/documentation/concepts/security_auth.rst @@ -1,6 +1,6 @@ -.. _data: +.. _security: -How is security taken into account? +Security aspects ==================================== Data @@ -18,7 +18,7 @@ data and time series for energy consumption/generation or weather). .. _auth: -Authentication and Authorization +Authentication and Authorisation --------------------------------- *Authentication* is the system by which users tell the FlexMeasures platform that they are who they claim they are. @@ -27,7 +27,7 @@ This involves a username/password combination ("credentials") or an access token * No user passwords are stored in clear text on any server - the FlexMeasures platform only stores the hashed passwords (encrypted with the `bcrypt hashing algorithm `_). If an attacker steals these password hashes, they cannot compute the passwords from them in a practical amount of time. * Access tokens are used so that the sending of usernames and passwords is limited (even if they are encrypted via https, see above) when dealing with the part of the FlexMeasures platform which sees the most traffic: the API functionality. Tokens thus have use cases for some scenarios, where developers want to treat authentication information with a little less care than credentials should be treated with, e.g. sharing among computers. However, they also expire fast, which is a common industry practice (by making them short-lived and requiring refresh, FlexMeasures limits the time an attacker can abuse a stolen token). At the moment, the access tokens on FlexMeasures platform expire after six hours. Access tokens are encrypted and validated with the `sha256_crypt algorithm `_, and `the functionality to expire tokens is realised by storing the seconds since January 1, 2011 in the token `_. The maximum age of access tokens in FlexMeasures can be altered by setting the env variable `SECURITY_TOKEN_MAX_AGE` to the number of seconds after which tokens should expire. -*Authorization* is the system by which the FlexMeasures platform decides whether an authenticated user can access a feature. For instance, many features are reserved for administrators, others for Prosumers (the owner of assets). +*Authorisation* is the system by which the FlexMeasures platform decides whether an authenticated user can access a feature. For instance, many features are reserved for administrators, others for Prosumers (the owner of assets). * This is achieved via *roles*. Each user has at least one role, but could have several, as well. * Roles cannot be edited via the UI at the moment. They are decided when a user is created. diff --git a/documentation/services.rst b/documentation/concepts/services.rst similarity index 85% rename from documentation/services.rst rename to documentation/concepts/services.rst index 4d468168b..a3f958791 100644 --- a/documentation/services.rst +++ b/documentation/concepts/services.rst @@ -3,7 +3,7 @@ .. _services: ************************************************** -Services of the FlexMeasures Platform +Services ************************************************** Monitoring @@ -31,7 +31,7 @@ All relevant data should be forecasted: Scheduling -------------- -The FlexMeasures platform optimizes schedules for your flexible assets. This is where energy flexibility is valorized! +The FlexMeasures platform optimises schedules for your flexible assets. This is where energy flexibility is valorised! Examples are: diff --git a/documentation/qa/users.rst b/documentation/concepts/users.rst similarity index 100% rename from documentation/qa/users.rst rename to documentation/concepts/users.rst diff --git a/documentation/conf.py b/documentation/conf.py index f014cfe5a..c678272de 100644 --- a/documentation/conf.py +++ b/documentation/conf.py @@ -77,8 +77,9 @@ # Todo: these are not mature enough yet for release exclude_patterns.append("int/*.rst") -exclude_patterns.append("qa/*.rst") -exclude_patterns.append("tut/*.rst") +exclude_patterns.append("concepts/assets.rst") +exclude_patterns.append("concepts/markets.rst") +exclude_patterns.append("concepts/users.rst") exclude_patterns.append("api/aggregator.rst") exclude_patterns.append("api/mdc.rst") exclude_patterns.append("api/prosumer.rst") @@ -176,7 +177,7 @@ f"{project} Documentation", author, project, - f"The {project} Platform is a tool for scheduling flexible energy actions on behalf of the connected asset owners.", + f"The {project} Platform is a tool for scheduling energy flexibility activations on behalf of the connected asset owners.", "Miscellaneous", ) ] diff --git a/documentation/index.rst b/documentation/index.rst index 8f91e90e1..13c671cd4 100644 --- a/documentation/index.rst +++ b/documentation/index.rst @@ -6,12 +6,12 @@ Planning ahead allows flexible assets to serve the whole system with their flexi e.g. by shifting or curtailing energy use. This can also be profitable for their owners. -The FlexMeasures Platform is a tool for scheduling flexible actions for energy assets. +The FlexMeasures Platform is a tool for scheduling flexibility activations for energy assets. For this purpose, it performs three services: * Monitoring of incoming measurements * Forecasting of expected measurements -* Scheduling flexible actions with custom optimization +* Scheduling flexibility activations with custom optimisation For more on FlexMeasures services, read :ref:`services`. Or head right over to :ref:`getting_started`. @@ -25,11 +25,6 @@ Therefore, this documentation uses USEF terminology, e.g. for role definitions. The intended users of FlexMeasures are a Supplier (energy company) and its Prosumers (asset owners who have energy contracts with that Supplier). The platform operator of FlexMeasures can be an Aggregator. -.. toctree:: - :caption: Purpose - :maxdepth: 1 - services - benefits .. toctree:: :maxdepth: 1 @@ -40,24 +35,25 @@ The platform operator of FlexMeasures can be an Aggregator. changelog .. toctree:: - :caption: The in-built UI + :caption: Concepts :maxdepth: 1 - views/dashboard - views/portfolio - views/analytics - views/admin + concepts/services + concepts/benefits + concepts/benefits_of_flex + concepts/algorithms + concepts/security_auth .. toctree:: + :caption: The in-built UI :maxdepth: 1 - :caption: Tutorials - - tut/supplier - tut/prosumer - tut/aggregator - tut/esco + views/dashboard + views/portfolio + views/control + views/analytics + views/admin .. toctree:: :caption: The API @@ -97,17 +93,6 @@ The platform operator of FlexMeasures can be an Aggregator. int/introduction -.. toctree:: - :caption: Narrative contents - :maxdepth: 2 - - qa/users - qa/assets - qa/markets - qa/algorithms - qa/data - - Code documentation ------------------ diff --git a/documentation/source.rst b/documentation/source.rst index 8ae239d94..9989bd2b8 100644 --- a/documentation/source.rst +++ b/documentation/source.rst @@ -6,6 +6,7 @@ Detailed documentation of all internal modules =============================================== +.. todo:: This is not yet complete, see `issue #52 `_ app diff --git a/documentation/tut/aggregator.rst b/documentation/tut/aggregator.rst deleted file mode 100644 index a1604d92d..000000000 --- a/documentation/tut/aggregator.rst +++ /dev/null @@ -1,5 +0,0 @@ -.. _aggregator: - -*************** -For Aggregators -*************** diff --git a/documentation/tut/esco.rst b/documentation/tut/esco.rst deleted file mode 100644 index a50884921..000000000 --- a/documentation/tut/esco.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _esco: - -**************************** -For Energy Service Companies -**************************** - diff --git a/documentation/tut/prosumer.rst b/documentation/tut/prosumer.rst deleted file mode 100644 index d79223651..000000000 --- a/documentation/tut/prosumer.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. _prosumer: - -************* -For Prosumers -************* - -- Go to the :ref:`dashboard` -- Set constraints in the :ref:`portfolio` page -- (Optional) Check analyses in the :ref:`analytics` page - diff --git a/documentation/tut/supplier.rst b/documentation/tut/supplier.rst deleted file mode 100644 index 60c3f4f77..000000000 --- a/documentation/tut/supplier.rst +++ /dev/null @@ -1,11 +0,0 @@ -.. _supplier: - -************* -For Suppliers -************* - -- Go to the the :ref:`dashboard` -- Check opportunities in the :ref:`portfolio` page -- Order flexibility in the :ref:`control` page -- (Optional) Check analyses in the :ref:`analytics` page - diff --git a/documentation/views/control.rst b/documentation/views/control.rst index abc931d3c..bbd9259a3 100644 --- a/documentation/views/control.rst +++ b/documentation/views/control.rst @@ -5,76 +5,33 @@ Flexibility opportunities ***************** Flexibility opportunities have commercial value that users can valorise on. -When FlexMeasures has identified commercial value for flexibility opportunities, the user is suggested to act on them. -This might happen manually (operators seeing opportunities in the FlexMeasures UI) or in an automated fashion -(scripts reading out suggested flexible schedules from the FlexMeasures API and implementing them to local operations if possible). +When FlexMeasures has identified commercial value of flexibility, the user is suggested to act on it. +This might happen in an automated fashion (scripts reading out suggested schedules from the FlexMeasures API and implementing them to local operations if possible) or manually (operators agreeing with the opportunities identified by FlexMeasures and acting on the suggested schedules). -In the Flexibility opportunities page (a yet-to-be designed feature discussed below), FlexMeasures shows all flexibility opportunities that the user can take for a selected time window. +For this latter case, in the Flexibility opportunities web-page (a yet-to-be designed UI feature discussed below), FlexMeasures could show all flexibility opportunities that the user can act on for a selected time window. .. contents:: :local: :depth: 1 -.. _opportunity_types: - -Types of flexibility opportunities -========================== - -The platform distinguishes between different types of flexibility opportunities and the type of action to take advantage of it. - -Curtailment ------------ - -Curtailment happens when an asset temporarily lowers or stops its production or consumption. -A defining feature of curtailment is that total production or consumption at the end of the flexibility opportunity has decreased. - -- A typical example of curtailing production is when a wind turbine adjusts the pitch angle of its blades to decrease the generator torque. -- An example of curtailing consumption is load shedding of energy intensive industries. - -Curtailment offers may specify some freedom in terms of how much energy can be curtailed. -In these cases, the user can select the energy volume (in MWh) to be ordered, within constraints set by the relevant Prosumer. -The net effect of a curtailment action is also measured in terms of an energy volume (see the flexibility metrics in the :ref:`portfolio` page). -Note that the volume ordered is not necessarily equal to the volume curtailed: -the ordered volume relates only to the selected time window, -while the curtailed volume may include volumes outside of the selected time window. -For example, an asset that runs an all-or-nothing consumption process of 2 hours can be ordered to curtail consumption for 1 hour, but will in effect stop the entire process. -In this case, the curtailed volume will be higher than the ordered volume, and the platform will take into account the total expected curtailment in its calculations. - -Shifting --------- - -Shifting happens when an asset delays or advances its energy production or consumption. -A defining feature of shifting is that total production or consumption at the end of the flexibility opportunity remains the same. - -- An example of delaying consumption is when a charging station postpones the charging process of an electric vehicle. -- An example of advancing consumption is when a cooling unit starts to cool before the upper temperature bound was reached (pre-cooling). - -Shifting offers may specify some freedom in terms of how much energy can be shifted. -In these cases, the user can select the energy volume (in MWh) to be ordered, within constraints set by the relevant Prosumer. -This energy volume represents how much energy is shifting into or out of the selected time window. -The net effect of a shifting action is measured in terms of an energy-time volume (see the flexibility metrics in the :ref:`portfolio` page). -This volume is a multiplication of the energy volume being shifted and the duration of that shift. - - Visualisation of opportunities ======================== Visualising flexibility opportunities and their effects is not straightforward. +Flexibility opportunities can cause changes to the power profile of an asset in potentially complex ways. +One example is called the rebound effect, where a decrease in consumption leads to an increase in consumption at a later point in time, because consumption is essentially postponed. +Such effects could be taken into account by FlexMeasures and shown to the user, e.g. as a part of expected value calculations and power profile forecasts. -Here is a potential UX design which we have not implemented yet: +Below is an example of what this could look like. +This is a potential UX design which we have not implemented yet. .. image:: https://github.com/SeitaBV/screenshots/raw/main/screenshot_control.png :align: center .. :scale: 40% -Flexibility opportunities cause changes to the power profile of an asset. -Such effects could be taken into account by FlexMeasures and shown to the user, e.g. as a part of expected value calculations and power profile forecasts. - -An example how this could look like is below. -The operator can select flexibility opportunities which have a value attached to them and see the effects on the power profile in a visual manner. - +The operator can select flexibility opportunities with an attached value, and see the effects on the power profile in a visual manner. Listed flexibility opportunities include previously realised opportunities and currently offered opportunities. -Currently offered opportunties are presented as an order book, where they are sorted according to their commercial value. +Currently offered opportunities are presented as an order book, where they are sorted according to their commercial value. -Of course, depending on the time window selection and constraints set by the asset owner, the effects of an opportunity may partially take place outside of the selected time window. \ No newline at end of file +Of course, depending on the time window selection and constraints set by the asset owner, the rebound effects of an opportunity may partially take place outside of the selected time window. \ No newline at end of file diff --git a/documentation/views/dashboard.rst b/documentation/views/dashboard.rst index 23dd11cb6..7f737426d 100644 --- a/documentation/views/dashboard.rst +++ b/documentation/views/dashboard.rst @@ -5,7 +5,7 @@ Dashboard ********* The dashboard shows where the user's assets are located and how many different asset types are connected to the platform. -The view serves to quickly identify the status of assets, such as whether there are upcoming opportunities to valorise on flexibility actions. +The view serves to quickly identify the status of assets, such as whether there are upcoming opportunities to valorise on flexibility activations. In particular, the page contains: .. contents:: @@ -24,7 +24,7 @@ Interactive map of assets ========================= The map shows all of the user's assets with icons for each asset type. -Clicking on an asset allows the user to see its current state (e.g. latest measurement of wind power production) and to navigate to the :ref:`analaytics` page +Clicking on an asset allows the user to see its current state (e.g. latest measurement of wind power production) and to navigate to the :ref:`analytics` page to see more details, for instance forecasts. @@ -34,5 +34,5 @@ Summary of asset types ====================== The summary below the map lists all asset types that the user has hooked up to the platform and how many of each there are. -Clicking on the asset type name leads to the :ref:`analaytics` page, where data is shown aggregated for that asset type. +Clicking on the asset type name leads to the :ref:`analytics` page, where data is shown aggregated for that asset type. diff --git a/documentation/views/portfolio.rst b/documentation/views/portfolio.rst index 7a541483f..e3da4833c 100644 --- a/documentation/views/portfolio.rst +++ b/documentation/views/portfolio.rst @@ -20,14 +20,14 @@ In particular, the page contains: .. :scale: 40% -.. _portfolio_financial_statements: +.. _portfolio_statements: -Financial statements about energy and flexibility opportunities +Statements about energy and flex activations ======================================================= The financial statements separate the effects of energy consumption/production and flexible schedules over two tables. -Statements about energy +Energy summary ----------------------- The top table lists the effects of energy trading for each asset type in the user's portfolio. @@ -36,17 +36,20 @@ Production and consumption values are total volumes within the selected time win Costs and revenues are calculated based on the relevant market prices for the user within the selected time window. A consumer will only have costs, while a prosumer may have both costs and revenues. -A supplier always has both costs and revenues, since it trades energy both with its customers and with external markets. -Finally, the financial statements show the total profit or loss per asset. +A supplier has revenues, since it sells energy to the other roles within FlexMeasures. + +Finally, the financial statements show the total profit or loss per asset type. + -Statements about flexible schedules +Market status ---------------------------------- +.. note:: This feature is mocked for now. The bottom table lists the effects of flexible schedules for each asset type in the user's portfolio. -Separate columns are stated for each type of scheduled deviation from the status quo, e.g. curtailment and shifting (see :ref:`opportunity_types`), with relevant total volumes within the selected time window. +Separate columns are stated for each type of scheduled deviation from the status quo, e.g. curtailment and shifting (see :ref:`flexibility_types`), with relevant total volumes within the selected time window. [#f1]_ -Costs and revenues are calculated based on an internal method for profit sharing (TODO: explain). +Costs and revenues are calculated based on the following internal method for profit sharing: Asset owners that follow flexible schedules via the platform will generate revenues. Suppliers that follow flexible schedules via the platform will generate both costs and revenues, where the revenues come from interacting with external markets. Finally, the financial statements show the total profit or loss per asset. diff --git a/flexmeasures/data/services/scheduling.py b/flexmeasures/data/services/scheduling.py index 60a8d0722..e47cd81a7 100644 --- a/flexmeasures/data/services/scheduling.py +++ b/flexmeasures/data/services/scheduling.py @@ -125,7 +125,7 @@ def make_schedule( ) else: raise ValueError( - "Scheduling is not supported for asset type %s." % asset.asset_type + "Scheduling is not (yet) supported for asset type %s." % asset.asset_type ) data_source = get_data_source( diff --git a/flexmeasures/data/services/time_series.py b/flexmeasures/data/services/time_series.py index 134e37f64..44fa20757 100644 --- a/flexmeasures/data/services/time_series.py +++ b/flexmeasures/data/services/time_series.py @@ -260,20 +260,6 @@ def convert_query_window_for_demo( return start, end -def convert_values_for_demo(values: pd.DataFrame, resolution: str, as_beliefs: bool): - values.index = values.index.map(lambda t: t.replace(year=datetime.now().year)) - if not values.empty: - values = values.reindex( - pd.date_range(values.index[0], values.index[-1], freq=resolution) - ) - if values.index[0].is_leap_year: - values["y"].loc[(values.index.month == 2) & (values.index.day == 29)] = 0 - if as_beliefs: - values["label"] = values["label"].fillna("") - values["horizon"] = values["horizon"].fillna({i: [] for i in values.index}) - return values - - def aggregate_values(bdf_dict: Dict[str, tb.BeliefsDataFrame]) -> tb.BeliefsDataFrame: # todo: test this function rigorously, e.g. with empty bdfs in bdf_dict diff --git a/flexmeasures/ui/templates/views/portfolio.html b/flexmeasures/ui/templates/views/portfolio.html index c31db6ba5..7feac15dc 100644 --- a/flexmeasures/ui/templates/views/portfolio.html +++ b/flexmeasures/ui/templates/views/portfolio.html @@ -77,6 +77,7 @@

Energy summary

+ {% if fm_mode == "demo" %}

Flexibility summary

@@ -103,15 +104,15 @@

Flexibility summary

title="{{ asset_types[asset_type_name].hover_label | capitalize }}" {% endif %}> {{ asset_type_name | capitalize }} - - - + {% endfor %} {% if asset_types | length > 1 %} @@ -119,14 +120,16 @@

Flexibility summary

- - - + + + {% endif %}
{% if asset_types[asset_type_name].is_producer and curtailment_per_asset_type[asset_type_name] > 0 %} - {{ "{:,.3f}".format( curtailment_per_asset_type[asset_type_name] ) }} + {% if asset_types[asset_type_name].is_producer and flex_info["curtailment_per_asset_type"][asset_type_name] > 0 %} + {{ "{:,.3f}".format( flex_info["curtailment_per_asset_type"][asset_type_name] ) }} {% endif %} {% if asset_types[asset_type_name].is_consumer and shifting_per_asset_type[asset_type_name] > 0 %} - {{ "{:,.3f}".format( shifting_per_asset_type[asset_type_name] ) }} + {% if asset_types[asset_type_name].is_consumer and flex_info["shifting_per_asset_type"][asset_type_name] > 0 %} + {{ "{:,.3f}".format( flex_info["shifting_per_asset_type"][asset_type_name] ) }} {% endif %} {{ "{:,.2f}".format( profit_loss_flexibility_per_asset_type[asset_type_name] ) }}{{ "{:,.2f}".format( flex_info["profit_loss_flexibility_per_asset_type"][asset_type_name] ) }}
Total {{ "{:,.3f}".format( sum_curtailment ) }}{{ "{:,.3f}".format( sum_shifting ) }}{{ "{:,.2f}".format( sum_profit_loss_flexibility ) }}{{ "{:,.3f}".format( flex_info["sum_curtailment"] ) }}{{ "{:,.3f}".format( flex_info["sum_shifting"] ) }}{{ "{:,.2f}".format( flex_info["sum_profit_loss_flexibility"] ) }}
{% endif %} + {% endif %} +

Market status

Market status

Portfolio status

+ {% if fm_mode == "demo" %}

For onshore and vehicles, an opportunity is mocked for the following 4am timeslot (select time window "today" or "tomorrow"). Double-click the opportunity to see the order book.

+ {% endif%}

You can also +

+ {% if fm_mode == "demo" %}
{{ portfolio_plots_divs[0] | safe }} @@ -179,6 +186,13 @@

Portfolio status

{{ portfolio_plots_divs[1] | safe }}
+ {% else %} +
+
+ {{ portfolio_plots_divs | safe }} +
+
+ {% endif %} {{ portfolio_plots_script | safe }}
@@ -197,9 +211,11 @@

Asset status

+ {% if fm_mode == "demo" %} + {% endif %} @@ -222,19 +238,21 @@

Asset status

{{ "{:,.2f}".format( consumption_per_asset[asset.name] ) }} MWh {% endif %} + {% if fm_mode == "demo" %} + {% endif %}
Production ConsumptionCurtailment Shifting Balancing revenue
- {% if asset.asset_type.can_curtail and curtailment_per_asset[asset.name] > 0 %} - {{ "{:,.2f}".format( curtailment_per_asset[asset.name] ) }} MWh + {% if asset.asset_type.can_curtail and flex_info["curtailment_per_asset"][asset.name] > 0 %} + {{ "{:,.2f}".format( flex_info["curtailment_per_asset"][asset.name] ) }} MWh {% endif %} - {% if asset.asset_type.can_shift and shifting_per_asset[asset.name] > 0 %} - {{ "{:,.2f}".format( shifting_per_asset[asset.name] ) }} MWh + {% if asset.asset_type.can_shift and flex_info["shifting_per_asset"][asset.name] > 0 %} + {{ "{:,.2f}".format( flex_info["shifting_per_asset"][asset.name] ) }} MWh {% endif %} - {{ "{:,.2f}".format( profit_loss_flexibility_per_asset[asset.name] ) }} KRW + {{ "{:,.2f}".format( flex_info["profit_loss_flexibility_per_asset"][asset.name] ) }} KRW
diff --git a/flexmeasures/ui/views/portfolio.py b/flexmeasures/ui/views/portfolio.py index ceda52182..f5c2592d0 100644 --- a/flexmeasures/ui/views/portfolio.py +++ b/flexmeasures/ui/views/portfolio.py @@ -1,13 +1,14 @@ -from datetime import timedelta +from datetime import datetime, timedelta from typing import Dict, List -from flask import request, session +from flask import request, session, current_app from flask_security import roles_accepted from flask_security.core import current_user import pandas as pd import numpy as np from bokeh.embed import components import bokeh.palettes as palettes +from bokeh.plotting import Figure from flexmeasures.data.models.assets import Asset, Power from flexmeasures.data.models.markets import Price @@ -83,14 +84,6 @@ def portfolio_view(): # noqa: C901 if sum_dict else pd.DataFrame() ) - stack_dict = ( - rename_event_value_column_to_resource_name(supply_resources_df_dict).values() - if show_summed == "consumption" - else rename_event_value_column_to_resource_name( - demand_resources_df_dict - ).values() - ) - df_stacked_data = pd.concat(stack_dict, axis=1) if stack_dict else pd.DataFrame() # Create summed plot power_sum_df = data_or_zeroes(power_sum_df, start, end, resolution) @@ -114,6 +107,14 @@ def portfolio_view(): # noqa: C901 fig_profile.plot_width = 900 # Create stacked plot + stack_dict = ( + rename_event_value_column_to_resource_name(supply_resources_df_dict).values() + if show_summed == "consumption" + else rename_event_value_column_to_resource_name( + demand_resources_df_dict + ).values() + ) + df_stacked_data = pd.concat(stack_dict, axis=1) if stack_dict else pd.DataFrame() df_stacked_data = data_or_zeroes(df_stacked_data, start, end, resolution) df_stacked_areas = stack_df(df_stacked_data) @@ -139,111 +140,20 @@ def portfolio_view(): # noqa: C901 level="underlay", ) - # Flexibility numbers are mocked for now - curtailment_per_asset = {a.name: 0 for a in assets} - shifting_per_asset = {a.name: 0 for a in assets} - profit_loss_flexibility_per_asset = {a.name: 0 for a in assets} - curtailment_per_asset_type = {k: 0 for k in represented_asset_types.keys()} - shifting_per_asset_type = {k: 0 for k in represented_asset_types.keys()} - profit_loss_flexibility_per_asset_type = { - k: 0 for k in represented_asset_types.keys() - } - shifting_per_asset["48_r"] = 1.1 - profit_loss_flexibility_per_asset["48_r"] = 76000 - shifting_per_asset_type["one-way EVSE"] = shifting_per_asset["48_r"] - profit_loss_flexibility_per_asset_type[ - "one-way EVSE" - ] = profit_loss_flexibility_per_asset["48_r"] - curtailment_per_asset["hw-onshore"] = 1.3 - profit_loss_flexibility_per_asset["hw-onshore"] = 84000 - curtailment_per_asset_type["wind turbines"] = curtailment_per_asset["hw-onshore"] - profit_loss_flexibility_per_asset_type[ - "wind turbines" - ] = profit_loss_flexibility_per_asset["hw-onshore"] - - # Add referral to mocked control action - this_hour = time_utils.get_most_recent_hour() - next4am = [ - dt - for dt in [this_hour + timedelta(hours=i) for i in range(1, 25)] - if dt.hour == 4 - ][0] + portfolio_plots_script, portfolio_plots_divs = components(fig_profile) - # TODO: show when user has (possible) actions in order book for a time slot - if current_user.is_authenticated and ( - current_user.has_role("admin") - or "wind" in current_user.email - or "charging" in current_user.email - ): - plotting.highlight( - fig_profile, next4am, next4am + timedelta(hours=1), redirect_to="/control" + # Flexibility numbers and a mocked control action are mocked for demo mode at the moment + flex_info = {} + if current_app.config.get("FLEXMEASURES_MODE") == "demo": + flex_info = mock_flex_info(assets, represented_asset_types) + fig_actions = mock_flex_figure( + x_range, power_sum_df.index, fig_profile.plot_width ) - - # actions - df_actions = pd.DataFrame(index=power_sum_df.index, columns=["event_value"]).fillna( - 0 - ) - if next4am in df_actions.index: - if current_user.is_authenticated: - if current_user.has_role("admin"): - df_actions.loc[next4am] = -2.4 # mock two actions - elif "wind" in current_user.email: - df_actions.loc[next4am] = -1.3 # mock one action - elif "charging" in current_user.email: - df_actions.loc[next4am] = -1.1 # mock one action - next2am = [ - dt - for dt in [this_hour + timedelta(hours=i) for i in range(1, 25)] - if dt.hour == 2 - ][0] - if next2am in df_actions.index: - if next2am < next4am and ( - current_user.is_authenticated - and ( - current_user.has_role("admin") - or "wind" in current_user.email - or "charging" in current_user.email - ) - ): - # mock the shift "payback" (actually occurs earlier in our mock example) - df_actions.loc[next2am] = 1.1 - next9am = [ - dt - for dt in [this_hour + timedelta(hours=i) for i in range(1, 25)] - if dt.hour == 9 - ][0] - if next9am in df_actions.index: - # mock some other ordered actions that are not in an opportunity hour anymore - df_actions.loc[next9am] = 3.5 - - fig_actions = plotting.create_graph( - df_actions, - unit="MW", - title="Ordered balancing actions", - x_range=x_range, - y_label="Power (in MW)", - ) - fig_actions.plot_height = 150 - fig_actions.plot_width = fig_profile.plot_width - fig_actions.xaxis.visible = False - - if current_user.is_authenticated and ( - current_user.has_role("admin") - or "wind" in current_user.email - or "charging" in current_user.email - ): - plotting.highlight( - fig_actions, next4am, next4am + timedelta(hours=1), redirect_to="/control" + mock_flex_action_in_main_figure(fig_profile) + portfolio_plots_script, portfolio_plots_divs = components( + (fig_profile, fig_actions) ) - portfolio_plots_script, portfolio_plots_divs = components( - (fig_profile, fig_actions) - ) - next24hours = [ - (time_utils.get_most_recent_hour() + timedelta(hours=i)).strftime("%I:00 %p") - for i in range(1, 26) - ] - return render_flexmeasures_template( "views/portfolio.html", assets=assets, @@ -252,25 +162,15 @@ def portfolio_view(): # noqa: C901 markets=markets, production_per_asset=production_per_asset, consumption_per_asset=consumption_per_asset, - curtailment_per_asset=curtailment_per_asset, - shifting_per_asset=shifting_per_asset, - profit_loss_flexibility_per_asset=profit_loss_flexibility_per_asset, production_per_asset_type=production_per_asset_type, consumption_per_asset_type=consumption_per_asset_type, - curtailment_per_asset_type=curtailment_per_asset_type, - shifting_per_asset_type=shifting_per_asset_type, - profit_loss_flexibility_per_asset_type=profit_loss_flexibility_per_asset_type, sum_production=sum(production_per_asset_type.values()), sum_consumption=sum(consumption_per_asset_type.values()), - sum_curtailment=sum(curtailment_per_asset_type.values()), - sum_shifting=sum(shifting_per_asset_type.values()), - sum_profit_loss_flexibility=sum( - profit_loss_flexibility_per_asset_type.values() - ), + flex_info=flex_info, portfolio_plots_script=portfolio_plots_script, portfolio_plots_divs=portfolio_plots_divs, - next24hours=next24hours, alt_stacking=show_summed, + fm_mode=current_app.config.get("FLEXMEASURES_MODE"), ) @@ -307,3 +207,121 @@ def rename_event_value_column_to_resource_name( df_name: df.rename(columns={"event_value": capitalize(df_name)}) for df_name, df in df_dict.items() } + + +def mock_flex_info(assets, represented_asset_types) -> dict: + flex_info = dict( + curtailment_per_asset={a.name: 0.0 for a in assets}, + shifting_per_asset={a.name: 0.0 for a in assets}, + profit_loss_flexibility_per_asset={a.name: 0.0 for a in assets}, + curtailment_per_asset_type={k: 0.0 for k in represented_asset_types.keys()}, + shifting_per_asset_type={k: 0.0 for k in represented_asset_types.keys()}, + profit_loss_flexibility_per_asset_type={ + k: 0.0 for k in represented_asset_types.keys() + }, + ) + + flex_info["shifting_per_asset"]["48_r"] = 1.1 + flex_info["profit_loss_flexibility_per_asset"]["48_r"] = 76000.0 + flex_info["shifting_per_asset_type"]["one-way EVSE"] = flex_info[ + "shifting_per_asset" + ]["48_r"] + flex_info["profit_loss_flexibility_per_asset_type"]["one-way EVSE"] = flex_info[ + "profit_loss_flexibility_per_asset" + ]["48_r"] + flex_info["curtailment_per_asset"]["hw-onshore"] = 1.3 + flex_info["profit_loss_flexibility_per_asset"]["hw-onshore"] = 84000.0 + flex_info["curtailment_per_asset_type"]["wind turbines"] = flex_info[ + "curtailment_per_asset" + ]["hw-onshore"] + flex_info["profit_loss_flexibility_per_asset_type"]["wind turbines"] = flex_info[ + "profit_loss_flexibility_per_asset" + ]["hw-onshore"] + + flex_info["sum_curtailment"] = sum(flex_info["curtailment_per_asset_type"].values()) # type: ignore + flex_info["sum_shifting"] = sum(flex_info["shifting_per_asset_type"].values()) # type: ignore + flex_info["sum_profit_loss_flexibility"] = sum( # type: ignore + flex_info["profit_loss_flexibility_per_asset_type"].values() # type: ignore + ) + return flex_info + + +def mock_flex_figure(x_range, x_index, fig_width) -> Figure: + df_actions = pd.DataFrame(index=x_index, columns=["event_value"]).fillna(0) + next_action_hour4 = get_flex_action_hour(4) + if next_action_hour4 in df_actions.index: + if current_user.is_authenticated: + if current_user.has_role("admin"): + df_actions.loc[next_action_hour4] = -2.4 # mock two actions + elif "wind" in current_user.email: + df_actions.loc[next_action_hour4] = -1.3 # mock one action + elif "charging" in current_user.email: + df_actions.loc[next_action_hour4] = -1.1 # mock one action + + next_action_hour2 = get_flex_action_hour(2) + if next_action_hour2 in df_actions.index: + if next_action_hour2 < next_action_hour4 and ( + current_user.is_authenticated + and ( + current_user.has_role("admin") + or "wind" in current_user.email + or "charging" in current_user.email + ) + ): + # mock the shift "payback" (actually occurs earlier in our mock example) + df_actions.loc[next_action_hour2] = 1.1 + + next_action_hour9 = get_flex_action_hour(9) + if next_action_hour9 in df_actions.index: + # mock some other ordered actions that are not in an opportunity hour anymore + df_actions.loc[next_action_hour9] = 3.5 + + fig_actions = plotting.create_graph( + df_actions, + unit="MW", + title="Ordered balancing actions", + x_range=x_range, + y_label="Power (in MW)", + ) + fig_actions.plot_height = 150 + fig_actions.plot_width = fig_width + fig_actions.xaxis.visible = False + + if current_user.is_authenticated and ( + current_user.has_role("admin") + or "wind" in current_user.email + or "charging" in current_user.email + ): + plotting.highlight( + fig_actions, + next_action_hour4, + next_action_hour4 + timedelta(hours=1), + redirect_to="/control", + ) + return fig_actions + + +def mock_flex_action_in_main_figure(fig_profile: Figure): + # show when user has (possible) actions in order book for a time slot + if current_user.is_authenticated and ( + current_user.has_role("admin") + or "wind" in current_user.email + or "charging" in current_user.email + ): + next_action_hour = get_flex_action_hour(4) + plotting.highlight( + fig_profile, + next_action_hour, + next_action_hour + timedelta(hours=1), + redirect_to="/control", + ) + + +def get_flex_action_hour(h: int) -> datetime: + """ get the next hour from now on """ + this_hour = time_utils.get_most_recent_hour() + return [ + dt + for dt in [this_hour + timedelta(hours=i) for i in range(1, 25)] + if dt.hour == h + ][0]