Layer 2: Nanogrids and SupplyAgreements

The Nanogrid is the core component of the system. The Nanogrid is the agent.
The code below is specifically configured for a home implementation and has been written in C#.
In our sample code our Nanogrid Agents are homes which may have a Load, Solar, and/or Battery Assets.
This layer is extended and complemented by the Holochain Layer which is used to handle the shared resources, transaction and validation workflows.
These however are not a necessary structure. The important element in the protocol is the workflow. How you use this workflow depends on the context of your individual application and use-case.
See the Applications section above for guidance on how to apply these code workflow elements to your solution.
In terms of that workflow the Core elements in the structure are;
[SerializeField] private float availableEnergy = 0f; //kWh
[SerializeField] private float requiredEnergy = 0f; //kWh
and;
private int timeInterval;
The availableEnergy and requiredEnergy are queried by the Nanogrid and used to determine if it will;
RequestEnergyPurchase() //or
BuyPower()
The RequestEnergyPurchase() call will iterate through the nanogrids list of supply agreements.
When the Nanogrid agent has iterated that list and 'requiredEnergy' is still >0 the agent will call the 'BuyPower()' function.

Nanogrid

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class NanoGrid : MonoBehaviour
{
private string nanoGridName;
public float size;
private int numberOfPanels = 0;
private float loadFactor = 1;
private float time = 0f;
private int timeInterval;
private int timeSpeedFactor;
private SolarPanel solar;
private Battery battery;
private Load load;
[SerializeField] private float availableEnergy = 0f; //kWh
[SerializeField] private float requiredEnergy = 0f; //kWh
private List<SupplyAgreement> supplyAgreements = new List<SupplyAgreement>();
private void Awake()
{
numberOfPanels = (int)(size / 30);
loadFactor = size / 100;
load = GetComponent<Load>();
nanoGridName = transform.name;
}
void Start()
{
timeInterval = GameManager.Instance.GetTimeInterval();
timeSpeedFactor = GameManager.Instance.GetTimeSpeedFactor();
Transform solarTransform = transform.Find("SolarPanels -" + nanoGridName.Remove(0, 10));
if (solarTransform != null)
{
solar = solarTransform.GetComponent<SolarPanel>();
}
Transform batteryTransform = transform.Find("Battery -" + nanoGridName.Remove(0, 10));
if (batteryTransform != null)
{
battery = batteryTransform.GetComponent<Battery>();
}
foreach (Transform child in transform)
{
if (child.name.StartsWith("Supply Agreement - "))
{
supplyAgreements.Add(child.GetComponent<SupplyAgreement>());
}
}
CalculatePowerRequirements();
}
private void Update()
{
time += Time.deltaTime * timeSpeedFactor;
if (time > timeInterval)
{
CalculatePowerRequirements();
time = 0;
}
}
private void CalculatePowerRequirements()
{
SetAvailableEnergy(0);
SetRequiredEnergy(0);
float instantLoad = 0; //kW
if (load != null) instantLoad = load.GetInstantLoad(loadFactor);
Debug.Log(GameManager.Instance.GetGameTimeNow().ToString("ddd d MMMM HH:mm") + " " + nanoGridName + " instant load " + instantLoad + " kW");
float instantSolarPower = 0; //kW
if (solar != null)
{
instantSolarPower = solar.GetInstantPower(numberOfPanels);
Debug.Log(GameManager.Instance.GetGameTimeNow().ToString("ddd d MMMM HH:mm") + " " + nanoGridName + " instant solar " + instantSolarPower + " kW");
}
float availableSolarPower = instantSolarPower - instantLoad; //kW
if (battery != null)
{
if (availableSolarPower > 0)
{
Debug.Log(GameManager.Instance.GetGameTimeNow().ToString("ddd d MMMM HH:mm") + " " + nanoGridName + " uses excess solar to charge their battery, remaining energy can be sold via supply agreements");
SetAvailableEnergy(battery.ChargeBattery(ConvertPowerToEnergy(availableSolarPower))); // kWh
}
else
{
Debug.Log(GameManager.Instance.GetGameTimeNow().ToString("ddd d MMMM HH:mm") + " " + nanoGridName + " load is greater than solar generation, battery is used to supplement solar, if is energy still required supply agreements will be executed");
SetRequiredEnergy(battery.DischargeBattery(ConvertPowerToEnergy(-1 * availableSolarPower))); // kWh
}
Debug.Log(GameManager.Instance.GetGameTimeNow().ToString("ddd d MMMM HH:mm") + " " + nanoGridName + " battery charge " + GetInstantBatteryPower() + " kWh");
}
else
{
if (availableSolarPower >= 0)
{
Debug.Log(GameManager.Instance.GetGameTimeNow().ToString("ddd d MMMM HH:mm") + " " + nanoGridName + " has no battery, excess energy not used by load can be sold via supply agreements");
SetAvailableEnergy(ConvertPowerToEnergy(availableSolarPower)); // kWh
}
else
{
Debug.Log(GameManager.Instance.GetGameTimeNow().ToString("ddd d MMMM HH:mm") + " " + nanoGridName + " has no battery and requires more energy from their supply agreements");
SetRequiredEnergy(ConvertPowerToEnergy(-1 * availableSolarPower)); // kWh
}
}
if (RequireEnergy()) // kWh
{
Debug.Log(GameManager.Instance.GetGameTimeNow().ToString("ddd d MMMM HH:mm") + " " + nanoGridName + " needs " + GetRequiredEnergy() + " kWh from their Supply Agreements");
// Get from Supply Agreements
float amountBought = 0f;
foreach (SupplyAgreement supplyAgreement in supplyAgreements)
{
float requiredEnergy = GetRequiredEnergy();
NanoGrid supplier = supplyAgreement.GetSupplierNanoGrid();
amountBought = supplier.RequestEnergyPurchase(GetRequiredEnergy(), supplyAgreement, this);
SetRequiredEnergy(requiredEnergy - amountBought);
if (!RequireEnergy()) break;
}
}
if (RequireEnergy())
{
// Get from Retailer
PowerRetailer.Instance.BuyPower(GetRequiredEnergy(), nanoGridName);
SetRequiredEnergy(0);
}
}
private float ConvertPowerToEnergy(float power)
{
return power * timeInterval / 3600;
}
public float GetInstantLoad()
{
if (load != null) return load.GetInstantLoad(loadFactor);
return 0;
}
public float GetInstantSolarPower()
{
if (solar != null ) return solar.GetInstantPower(numberOfPanels);
return 0;
}
public float GetInstantBatteryPower()
{
if (battery != null) return battery.GetBatteryCharge();
return 0;
}
private void SetAvailableEnergy(float availableEnergy)
{
this.availableEnergy = availableEnergy;
}
private float GetAvailableEnergy()
{
return availableEnergy;
}
private float RequestEnergyPurchase(float requiredEnergy, SupplyAgreement supplyAgreement, NanoGrid consumer)
{
float supplyAgreementLimit = ConvertPowerToEnergy(supplyAgreement.GetTransactionEnergyLimit());
requiredEnergy = Mathf.Clamp(requiredEnergy, 0, supplyAgreementLimit);
Debug.Log(GameManager.Instance.GetGameTimeNow().ToString("ddd d MMMM HH:mm") + " " + consumer.nanoGridName + " RequestEnergyPurchase " + requiredEnergy + " kWh from " + this.nanoGridName + " limit = " + supplyAgreementLimit + " has " + GetAvailableEnergy() + " available");
if (requiredEnergy < GetAvailableEnergy())
{
SetAvailableEnergy(availableEnergy - requiredEnergy);
// Record transaction
Debug.Log(GameManager.Instance.GetGameTimeNow().ToString("ddd d MMMM HH:mm") + " " + consumer.nanoGridName + " bought all " + requiredEnergy + " kWh from " + this.nanoGridName);
return requiredEnergy;
} else
{
float amountSold = GetAvailableEnergy();
SetAvailableEnergy(0);
// Record transaction
Debug.Log(GameManager.Instance.GetGameTimeNow().ToString("ddd d MMMM HH:mm") + " " + consumer.nanoGridName + " bought " + amountSold + " kWh from " + this.nanoGridName);
return amountSold;
}
}
private void SetRequiredEnergy(float requiredEnergy)
{
this.requiredEnergy = requiredEnergy;
}
private float GetRequiredEnergy()
{
return requiredEnergy;
}
private bool RequireEnergy()
{
return requiredEnergy > 0;
}
}
As described above the Nanogrid agent maintains an ordered list of SupplyAgreements.
foreach (SupplyAgreement supplyAgreement in supplyAgreements)
{
float requiredEnergy = GetRequiredEnergy();
NanoGrid supplier = supplyAgreement.GetSupplierNanoGrid();
amountBought = supplier.RequestEnergyPurchase(GetRequiredEnergy(), supplyAgreement, this);
SetRequiredEnergy(requiredEnergy - amountBought);
These are individual agreements with Peers for the provision of Energy Services.
When a Nanogrid calls the 'RequestEnergyPurchase()' function that agent will iterate through that list.
A SupplyAgreement has a name, a cost (tariffIoenFuel), and an amount(transactionEnergyLimit).
[SerializeField] private NanoGrid supplierNanoGrid;
[SerializeField] private float tariffIoenFuel;
[SerializeField] private float transactionEnergyLimit;
The Energy Limit above is dynamically calculated by the Supplier Nanogrid agent based on their available energy for the interval. All other values are set and signed by all contract participants.
Note: A core component and differentiator of the Holochain platform is the validation capability. As we mature the codebase we will also add ''validation'' capability into the SupplyAgreement features below.

SupplyAgreement

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SupplyAgreement : MonoBehaviour
{
[SerializeField] private NanoGrid supplierNanoGrid;
[SerializeField] private float tariffIoenFuel;
[SerializeField] private float transactionEnergyLimit;
public void SetSupplierNanoGrid(NanoGrid supplierNanoGrid)
{
this.supplierNanoGrid = supplierNanoGrid;
}
public NanoGrid GetSupplierNanoGrid()
{
return supplierNanoGrid;
}
public void SetTariffIoenFuel(float tariffIoenFuel)
{
this.tariffIoenFuel = tariffIoenFuel;
}
public float GetTariffIoenFuel()
{
return tariffIoenFuel;
}
public void SetTransactionEnergyLimit(float transactionEnergyLimit)
{
this.transactionEnergyLimit = transactionEnergyLimit;
}
public float GetTransactionEnergyLimit()
{
return transactionEnergyLimit;
}
}
Finally once the Nanogrid agent has exhausted its list of SupplyAgreements it will activate the ''BuyPower()'' function.
This function can be considered the ''provider of last resort'' for that agent when they cannot get the energy they need locally.
In most cases this option would involve purchasing power from a Retailer or non preferred option.

BuyPower

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PowerRetailer : MonoBehaviour
{
public static PowerRetailer Instance { get; private set; }
private Dictionary<string, float> customers;
private int timeInterval;
private void Awake()
{
Instance = this;
customers = new Dictionary<string, float>();
}
private void Start()
{
timeInterval = GameManager.Instance.GetTimeInterval();
}
public float BuyPower(float amount, string name)
{
if (!customers.ContainsKey(name))
{
customers[name] = 0f;
}
customers[name] += amount;
// Record transaction
Debug.Log(GameManager.Instance.GetGameTimeNow().ToString("ddd d MMMM HH:mm") + " " + name + " buys " + amount + " kWh from PowerRetailer - total " + customers[name] + "kWh");
return 0;
}
}