Shaokang's Blog

In a system where multiple types of energy source exist, running all components at all available times could be a waste by considering multiple different factors, including the device depreciation, normal energy in maintaining the entire system running. This waste is a significant factor when lower wind for wind producer happen, or lower light for light producer happen, or other stuff.

This is a web based energy running simulation simulator with jsLpsolver to find optimal threshold, which will be used to determine the time to turn on the device and time to turn off. It contains methods to analyze history data to predict a threshold to make the entire system running as efficient as it could. After proper data is provided, system will run based on a threshold setting from user or automatically analyzed based on history data so that no energy will be wasted. This app is done solely by me. More details is as below:

Ways of analyzing history data

If Using the first 30 input data to optimize and get the best possible threshold. is checked, the first 30 data will be used to generate the optimized threshold.

By comparing methods in passage Optimal Energy Production designed by me and the usage case, the following one is chosen:

Define variables

J : type of energy, JJ\in {wind, water, \cdots }

N : value index, 1, 2, \cdots.

C = RJ\mathbf{R}^J: the normal energy consumption per unit time for each producer (Need to consider as a combination of multiple equipments, like: if we have two wind machine(user is able to define this in control), each machine will consume 10 W. Then CwindC_{wind}=10*2=20)

U = R\mathbf{R}: Total user consumption

W = RJ,N\mathbf{R}^{J,N}, the expected energy(total in above table) produced in each value index, either pre-calculated or calculate in model.

S = {0,1}J,N^{J,N}, to indicate I should turn on wind on this statistic data or not

B R\in \mathbf{R} : The battery charging limitation.

D RJ,N\in \mathbf{R}^{J,N}, indicate the day count each value index, either pre-calculated or calculate in model.

Optimization Model

\begin{equation}\begin{split}&\text{Minimize } & \sum_{i\in N}\sum_{j\in J}D_{ij}S_{ij}\\& \text{Subject to}\ & 0 \le S_{ij} \le 1 \ \forall i\in N j\in J\\& & \sum_{i\in N}\sum_{j\in J}W_{ij}S_{ij}-\sum_{i\in N}\sum_{j\in J}C_{j}S_{ij}-U\ge B\end{split}\end{equation}

Use MIP to solve it

Code to solve it

This code including reading data from file using papaparse, papaparse also contains ways to read stream. Such that streaming data would be easily handled in the real situation. And the optimization is solved by using jsLpsolver.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
var days = 0;
var model = {};
var idx = 0;
var result;
var freq = parseFloat(document.getElementById("freq").value) / 3600000;
console.log(freq);
model["optimize"] = "count";
model["opType"] = "min";
model["constraints"] = {};
model["variables"] = {};
model["ints"] = {};
var data_input = {};
Papa.parse(file, {
header: true,
skipEmptyLines: true,
dynamicTyping: true,
step: function(results, parser) {
//console.log("Row data:", results.data);
//analyze data at here
//use == " " || == null to check empty
if (!(results.data["wind_value"] == " " || results.data["wind_value"] == null)) {
var variable_wind = "v" + idx + "0";
data_input[variable_wind] = results.data["wind_value"];
model.ints[variable_wind] = 1;
model.variables[variable_wind] = {
"value": results.data["wind_value"] * results.data["wind_count"] * freq,
"count": results.data["wind_count"]
};
model.variables[variable_wind][variable_wind] = 1;
model.constraints[variable_wind] = {
"min": 0,
"max": 1
};
days += results.data["wind_count"];
}

if (!(results.data["light_value"] == " " || results.data["light_value"] == null)) {
var variable_light = "v" + idx + "1";
data_input[variable_light] = results.data["light_value"];
model.ints[variable_light] = 1;
model.variables[variable_light] = {
"value": results.data["light_value"] * results.data["light_count"] * freq,
"count": results.data["light_count"]
};
model.variables[variable_light][variable_light] = 1;
model.constraints[variable_light] = {
"min": 0,
"max": 1
};
}

if (!(results.data["wave_value"] == " " || results.data["wave_value"] == null)) {
var variable_wave = "v" + idx + "2";
data_input[variable_wave] = results.data["wave_value"];
model.ints[variable_wave] = 1;
model.variables[variable_wave] = {
"value": results.data["wave_value"] * results.data["wave_count"] * freq,
"count": results.data["wave_count"]
};
model.variables[variable_wave][variable_wave] = 1;
model.constraints[variable_wave] = {
"min": 0,
"max": 1
};
}

if (!(results.data["current_value"] == " " || results.data["current_value"] == null)) {
var variable_current = "v" + idx + "3";
data_input[variable_current] = results.data["current_value"];
model.ints[variable_current] = 1;
model.variables[variable_current] = {
"value": results.data["current_value"] * results.data["current_count"] * freq,
"count": results.data["current_count"]
};
model.variables[variable_current][variable_current] = 1;
model.constraints[variable_current] = {
"min": 0,
"max": 1
};
}
idx++;
},
complete: function(results, parser) {
console.log(parseFloat(document.getElementById("User").value));
console.log(parseInt(document.getElementById("battery").value));
var limits = days * parseFloat(document.getElementById("User").value) + parseInt(document.getElementById("battery").value);
model.constraints["value"] = {
"min": limits
};
console.log(model);
result = solver.Solve(model);

console.log(result);

var wind_limit = -1;
var light_limit = -1;
var wave_limit = -1;
var current_limit = -1;

for (var key in result) {
if (data_input.hasOwnProperty(key)) {
console.log(key + " -> " + data_input[key]);
switch (parseInt(key.slice(-1))) {
case 0:
if (wind_limit == -1) wind_limit = data_input[key];
if (data_input[key] <= wind_limit) wind_limit = data_input[key];
break;
case 1: //light
if (light_limit == -1) light_limit = data_input[key];
if (data_input[key] <= light_limit) light_limit = data_input[key];
// code block
break;
case 2: //wave
if (wave_limit == -1) wave_limit = data_input[key];
if (data_input[key] <= wave_limit) wave_limit = data_input[key];
// code block
break;
case 3: //current
if (current_limit == -1) current_limit = data_input[key];
if (data_input[key] <= current_limit) current_limit = data_input[key];
// code block
break;

default:
// code block
}
}
}

Running

Basically, this page is served as a Github page, you could visit at shaokangjiang.github.io/energy/.

This website also has a lightweight localization, which will switch between ZH-CN and EN-US based on the system configuration. This is a pure static page with JavaScript. So anybody is able to host it easily on your own. If you don’t need to see some 3d effects, just download and click on each file would work. 3D effects require some cross-server interaction.

To run it and see the 3d effects, the way I use is to install Node.js at first. Then install the http-server using npm install --global http-server. Then do the following in any directory:

1
2
3
git clone https://github.com/ShaokangJiang/energy.git
cd energy
http-server

Then go to browser and go to 127.0.0.1:8080 should work.

Interactive content

3D model

3D model with real time running information is available while running. This is done by using A-Frame with Three.js

AR

By scanning specific pattern, it is able to load model in reality. /ar.html and /ar1.html provide two different ways, one is scanning via QR code, the other way is through scanning the 2D version of this 3D model, this is done based on machine learning of the configured pattern.

Wonderful charts

When hovering mouse on each card, it is able to plot the energy production in the past 10 time durations.

Data format:

Simulation data

Header should be the same, User_Usage is not required but recommend. Each field represent the value sensor received.

1
2
3
4
5
Time,Wind_Speed,Light_H,Wave_Hight,Wave_Period,Current_Speed,User_Usage
0,5.405,1213.072,3.591,9.104,1.846,501.5935
0.2,8.157,1191.057,3.022,10.431,1.908,497.0755
0.4,6.012,1197.794,1.709,8.784,1.936,503.0145
0.6,6.477,1205.489,2.1,9.857,1.858,499.4511

Download a sample from here.

Optimization data

Header should be the same, idx is not required. Each category has a value component recoding the amount of power produced in unit time. Each count means the possibility of happening. Because we need to use a KWH = b KW * s/3600 to calculate energy produced. The count field has to be integer. We could use the possibility * a factor to make it become integer. And the sum of each count should be the same.

1
2
3
4
5
6
7
idx,wind_value,wind_count,light_value,light_count,wave_value,wave_count,current_value,current_count
0,4962654.905,189,100.8496541,45,485.3900022,222,71501.61128,105
1,424133.8855,45,105.8598321,124,328.6975665,43,49752.79798,187
2,3414695.961,18,113.1989172,130,438.1380705,33,35910.75581,12
3,7346795.761,90,116.4237558,11,483.7242291,24,48819.12566,25
4,4033868.032,20,105.5786618,25,226.1068017,14,1611.378029,35
5, , ,106.0583865,19,364.8009734,10,19265.12771,1

Download a sample from here.

Generate Fake Simulation and optimization data

Fake simulation data

Use java code below to generate fake simulation data(to be used in run page).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static Random r = new Random();
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
FileWriter a = new FileWriter(new File("1.csv"));
a.write("Wind_Speed,Light_H,Wave_Hight,Wave_Period,Current_Speed\r\n");
double time = 0;
while(time<36) {
a.write(generateNormalVar(8,2)+","+generateNormalVar(1200,50)+
","+generateNormalVar(2.5,0.25)+","+generateNormalVar(9,0.5)+","+generateNormalVar(2,0.5)+"\r\n");
time+=0.2;
}
a.flush();
a.close();
}

/**
* N(a,b)
* center,distribution
* @param x
* @param y
* @return
*/
public static String generateNormalVar(double a, double b) {
double x = Math.sqrt(b)*r.nextGaussian()+a;
r.nextGaussian();
return String.format("%.3f", x);
}

Fake optimization data

  • To make the input source be easy to manage, represent and maintain. We could save them in the same file, if no enough data for a cell, leave it empty would be fine and recommended.

  • idx is not required but recommended

  • This generation code might be good enough as this is not the real case

  • header: idx,wind_value,wind_count,light_value,light_count,wave_value,wave_count,current_value,current_count

Use java code below to generate fake optimization data(to be used in start page):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public static void main(String[] args) throws IOException {
FileWriter a = new FileWriter(new File("1.csv"));
a.write("idx,wind_value,wind_count,light_value,light_count,wave_value,wave_count,current_value,current_count\r\n");
int count = 0;
int year = 365;
int wind_count=year,light_count=year,wave_count=year,current_count=year;
while(wind_count>0 || light_count>0 || wave_count>0 || current_count>0 ) {
int wind_count_=generateRandomIntegerIn(wind_count),light_count_=generateRandomIntegerIn(light_count),
wave_count_=generateRandomIntegerIn(wave_count),current_count_=generateRandomIntegerIn(current_count);
wind_count -= wind_count_;
light_count -= light_count_;
wave_count -= wave_count_;
current_count -= current_count_;
if(wind_count_==0&&light_count_==0&&wave_count_==0&&current_count_==0) continue;
double Wind_Speed=generateRandomDoubleIn(4,15),Light_H=generateRandomDoubleIn(1100,1300),
Wave_Hight=generateRandomDoubleIn(2,3),Wave_Period=generateRandomDoubleIn(7,11),
Current_Speed=generateRandomDoubleIn(1,3);
System.out.println(count+""+wind_count+""+light_count+""+wave_count+""+current_count+"");
a.write(count + ","+ (wind_count_ == 0 ? " " : 2866*Wind_Speed*Wind_Speed*Wind_Speed) +","+(wind_count_ == 0 ? " " : wind_count_) +","+
(light_count_ == 0 ? " " : 0.09*Light_H) +","+(light_count_ == 0 ? " " : light_count_) +","+
(wave_count_ == 0 ? " " : 6.6*Wave_Hight*Wave_Hight*Wave_Period)+","+(wave_count_ == 0 ? " " : wave_count_)+","+
(current_count_ == 0 ? " " : 1254*Current_Speed*Current_Speed*Current_Speed )+","+(current_count_ == 0 ? " " : current_count_)+"\r\n");
count++;
}
a.flush();
a.close();
}

//0~h
public static int generateRandomIntegerIn(int h) {
return ThreadLocalRandom.current().nextInt(0, h + 1);
}

public static double generateRandomDoubleIn(int Min, int Max) {
return Min + (Math.random() * ((Max - Min) + 1));
}

Usage

Each field means the threshold of each component except wave. In the case of wave, the threshold needs to be pre-calculated using $H^2T$. User usage is in kwh per day or unit time, the unit time refers to the refresh frequency, which will be used to calculate energy produced in the frame of time.

The way to calculate energy produced per unit time: a KWH = b KW*s/3600, a is the result, b is the current power, a is the calculated energy. s is the refresh frequency. Refresh frequency in this form is in ms(milliseconds).

In the manual setting, you are able to choose to provide user usage data in the future, usually this means to have a column of data recording user consumption per unit time, it will be used as a source of consumming power. If that file doesn't have user usage, you could set it at here. In the auto generate mode, you have to provide one to let us find the optimal solution(guess a possible one would be fine).

Calculation Equation

Wave: $P_{total}=0.6*11*H^2T=6.6H^2T$ H - height of wave T - period

Ocean Current: $P=\frac{\rho v^3 S C_p}{2}=\frac{1050 v^3 \pi1.3^2* 0.45}{2}=1254v^3$ P is the energy captured by the unit, ρ is the fluid density, V is the fluid velocity, and S is the swept area of the impeller$C_p$ is the energy utilization coefficient

Light: $P=\frac{798*H_A}{365*24}=0.09*H_A, H_A$ Total solar radiation per unit area

Wind: $P=\frac{\rho v^3 S C_p}{2}=\frac{1.293 v^3 9852* 0.45}{2}=2866v^3$ P is the energy captured by the unit, ρ is the fluid density, V is the fluid velocity, and S is the swept area of the impeller$C_p$ is the energy utilization coefficient

Requirement

Because I used the class technique to build LinkedList to use in generating charts, IE is not supported. And there are some requirements for the majority browser as well, see compatibility data at NPM official site. Other than the LinkedList, most of features should work in IE 11.

JS libraries used

ChangeLog

  1. 5/25/2020 Core running function finished & Time field in input simulation file is not required

  2. 5/26/2020 Multi-language added

  3. 5/27/2020 Optimize font and icon loading

  4. 5/29/2020 Optimization to find best history result function added & multi-file reading is not allowed in run page now

  5. 5/30/2020 User can now use the first 30 seconds of data to run and optimize threshold.

  6. 5/31/2020 More verification, notification added. Framework for displaying VR and AR is ready. Supported via static webpage, qrCode, and also in running page.

    For AR in static page, your device needs meet WebXR requirement:

    AR is also a standard and you can experiment today on Android ARCore compatible devices and Chrome 79 or newer.
    Enable the experimental WebXR AR module in chrome://flags

  7. 6/6/2020 All parts of 3d model is done.

Sample running

Optimization is done automatically and is not shown in this demo.

Demo could be view at Onedrive video

 Comments