urbanopt-rnm-us 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +3 -1
- data/CHANGELOG.md +5 -0
- data/Gemfile +6 -5
- data/README.md +15 -1
- data/Rakefile +31 -6
- data/lib/urbanopt/rnm/api_client.rb +8 -0
- data/lib/urbanopt/rnm/consumers.rb +14 -2
- data/lib/urbanopt/rnm/input_files.rb +18 -0
- data/lib/urbanopt/rnm/prosumers.rb +17 -1
- data/lib/urbanopt/rnm/validation/main_validation.py +135 -33
- data/lib/urbanopt/rnm/validation/opendss_interface.py +458 -79
- data/lib/urbanopt/rnm/validation/plot_lib.py +430 -124
- data/lib/urbanopt/rnm/validation/report.py +370 -0
- data/lib/urbanopt/rnm/validation.rb +8 -6
- data/lib/urbanopt/rnm/version.rb +1 -1
- data/requirements.txt +9 -0
- data/urbanopt-rnm-us-gem.gemspec +1 -1
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 07dfa016051b9b8499a11ba5d8066a73c06131d628af10adb2dfc8c9848e7c30
|
4
|
+
data.tar.gz: a6e9f7af3ee4e92951d9225caa5eddfb8f97b20b0e1a5d95898a3f4a132405e3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b25a77720880cc85ed29adabb35280a8e308692004183ef7da0a70ae0f4dd4e56c18d5299697490e8ed98e62e8007465ec31f80279c505595df23b144cbf16e5
|
7
|
+
data.tar.gz: e6a40b39b6f1f20f96df5379bf02c73205e00981e60bdf25dccaadb8780dd24522e18dcf423f3497fa7c03f6dd180cdc54531bd150666f2a33612b3a2b40c883
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
data/Gemfile
CHANGED
@@ -9,8 +9,9 @@ allow_local = ENV['FAVOR_LOCAL_GEMS']
|
|
9
9
|
# Below is an example of how to configure the gemfile for developing with local gems
|
10
10
|
# modify as appropriate
|
11
11
|
|
12
|
-
if allow_local && File.exists?('../urbanopt-geojson-gem')
|
13
|
-
|
14
|
-
elsif allow_local
|
15
|
-
|
16
|
-
end
|
12
|
+
# if allow_local && File.exists?('../urbanopt-geojson-gem')
|
13
|
+
# gem 'urbanopt-geojson', path: '../urbanopt-geojson-gem'
|
14
|
+
# elsif allow_local
|
15
|
+
# gem 'urbanopt-geojson', github: 'URBANopt/urbanopt-geojson-gem', branch: 'develop'
|
16
|
+
# end
|
17
|
+
|
data/README.md
CHANGED
@@ -47,7 +47,21 @@ bundle exec rake create_opendss_catalog[/desired/path/to/opendss_catalog.json]
|
|
47
47
|
| API Version | RNM-US Gem Version | RNM-US exe Version |
|
48
48
|
| ----------- | ----------- | ---------------- |
|
49
49
|
| v1 | 0.3.0 and earlier | RNM-US_20220819 |
|
50
|
-
| v2 | 0.4.0 | RNM-
|
50
|
+
| v2 | 0.4.0 | RNM-US_20221018 |
|
51
|
+
|
52
|
+
|
53
|
+
## Validation Functionality
|
54
|
+
|
55
|
+
The validation and results visualization functionality is written in python. Follow these steps if you would like to use it.
|
56
|
+
|
57
|
+
1. Install python (3.10) if you do not already have it installed
|
58
|
+
1. Clone the repo to your computer
|
59
|
+
1. cd into the repo directory
|
60
|
+
1. run `bundle install` to install the required ruby dependencies
|
61
|
+
1. run `pip install -r requirements.txt` to install the required python dependencies for the validation module
|
62
|
+
1. create input files and run the simulation as usual
|
63
|
+
1. run `bundle exec rake run_validation[/path/to/scenario/csv]` to run the validation
|
64
|
+
|
51
65
|
|
52
66
|
## Testing
|
53
67
|
|
data/Rakefile
CHANGED
@@ -148,6 +148,29 @@ task :run_simulation, [:scenario_csv_path, :reopt, :use_localhost] do |t, args|
|
|
148
148
|
puts '...done!'
|
149
149
|
end
|
150
150
|
|
151
|
+
# Full Runner workflow (mimics UO CLI functionality)
|
152
|
+
# pass in the path to the scenario csv, geojson path, whether this is a reopt analysis (true/false), and whether to use localhost RNM API (true/false)
|
153
|
+
desc 'Full Runner workflow'
|
154
|
+
task :full_runner_workflow, [:scenario_csv_path, :geojson_path, :reopt, :use_localhost] do |t, args|
|
155
|
+
# todo: could allow passing in extended catalog, average peak catalog, and opendss_catalog flags too
|
156
|
+
# if no path passed in, use default:
|
157
|
+
scenario_csv = args[:scenario_csv_path] || 'spec/test/example_project/run/baseline_scenario'
|
158
|
+
geojson_path = args[:geojson_path] || 'spec/test/example_project/example_project_with_network_and_streets'
|
159
|
+
root_dir, scenario_file_name = File.split(File.expand_path(scenario_csv))
|
160
|
+
scenario_name = File.basename(scenario_file_name, File.extname(scenario_file_name))
|
161
|
+
run_dir = File.join(root_dir, 'run', scenario_name.downcase)
|
162
|
+
reopt = args[:reopt] || false
|
163
|
+
reopt = reopt == 'true'
|
164
|
+
use_local = args[:use_localhost] || false
|
165
|
+
|
166
|
+
runner = URBANopt::RNM::Runner.new(scenario_name, run_dir, scenario_csv, geojson_path, reopt: reopt)
|
167
|
+
runner.create_simulation_files
|
168
|
+
runner.run(use_local)
|
169
|
+
runner.post_process
|
170
|
+
|
171
|
+
puts '...done!'
|
172
|
+
end
|
173
|
+
|
151
174
|
# Create opendss catalog from extended catalog
|
152
175
|
# pass in the path and filename where the OpenDSS catalog should be saved
|
153
176
|
desc 'Create OpenDSS catalog'
|
@@ -169,20 +192,22 @@ end
|
|
169
192
|
# run validation
|
170
193
|
# pass in the path to the scenario csv
|
171
194
|
desc 'Run Validation'
|
172
|
-
task :run_validation, [:scenario_csv_path, :
|
195
|
+
task :run_validation, [:scenario_csv_path, :use_numeric_ids] do |t, args|
|
173
196
|
#Exammple to run validation
|
174
|
-
#bundle exec rake run_validation[D:/.../urbanopt-rnm-us-gem/spec/files/example_project/baseline_scenario.csv]
|
197
|
+
#bundle exec rake run_validation[D:/.../urbanopt-rnm-us-gem/spec/files/example_project/baseline_scenario.csv,true]
|
198
|
+
|
175
199
|
puts 'Running OpenDSS validation'
|
200
|
+
|
176
201
|
# if no path passed in, use default:
|
177
202
|
scenario_csv = args[:scenario_csv_path] || 'spec/test/example_project/run/baseline_scenario'
|
178
203
|
root_dir, scenario_file_name = File.split(File.expand_path(scenario_csv))
|
179
204
|
scenario_name = File.basename(scenario_file_name, File.extname(scenario_file_name))
|
180
205
|
run_dir = File.join(root_dir, 'run', scenario_name.downcase)
|
181
|
-
|
182
206
|
rnm_dir = File.join(run_dir, 'rnm-us')
|
183
207
|
|
184
|
-
|
185
|
-
|
208
|
+
#Use numeric ids (for the hierarchical plot of the network)
|
209
|
+
use_numeric_ids = args[:use_numeric_ids] || false
|
210
|
+
use_numeric_ids = use_numeric_ids == 'true'
|
186
211
|
|
187
212
|
if !File.exist?(rnm_dir)
|
188
213
|
puts rnm_dir
|
@@ -190,7 +215,7 @@ task :run_validation, [:scenario_csv_path, :reopt, :use_localhost] do |t, args|
|
|
190
215
|
end
|
191
216
|
|
192
217
|
puts "run dir path: #{run_dir}"
|
193
|
-
validation = URBANopt::RNM::Validation.new(rnm_dir)
|
218
|
+
validation = URBANopt::RNM::Validation.new(rnm_dir,use_numeric_ids)
|
194
219
|
validation.run_validation()
|
195
220
|
|
196
221
|
puts '...done!'
|
@@ -108,6 +108,7 @@ module URBANopt
|
|
108
108
|
end
|
109
109
|
|
110
110
|
if !missing_files.empty?
|
111
|
+
puts "RNM DIR: #{@rnm_dir}"
|
111
112
|
raise "Input Files missing in directory: #{missing_files.join(',')}"
|
112
113
|
end
|
113
114
|
|
@@ -281,6 +282,13 @@ module URBANopt
|
|
281
282
|
# delete zip
|
282
283
|
File.delete(file_path)
|
283
284
|
|
285
|
+
# check if zip is empty
|
286
|
+
if Dir.empty? File.join(@rnm_dir, 'results')
|
287
|
+
msg = "Error in simulation: Results.zip empty"
|
288
|
+
@@logger.error(msg)
|
289
|
+
raise msg
|
290
|
+
end
|
291
|
+
|
284
292
|
else
|
285
293
|
msg = "Error retrieving results for #{the_sim_id}. error code: #{resp.status}. #{resp.body}"
|
286
294
|
@@logger.error(msg)
|
@@ -45,7 +45,7 @@ require 'csv'
|
|
45
45
|
module URBANopt
|
46
46
|
module RNM
|
47
47
|
class Consumers
|
48
|
-
attr_accessor :customers, :customers_ext, :profile_customer_p, :profile_customer_q, :profile_customer_p_ext, :profile_customer_q_ext, :power_factor
|
48
|
+
attr_accessor :customers, :customers_ext, :profile_customer_p, :profile_customer_q, :profile_customer_p_ext, :profile_customer_q_ext, :power_factor, :profile_date_time, :profile_date_time_ext
|
49
49
|
|
50
50
|
# initializing all the attributes to build the inputs files required by the RNM-US model
|
51
51
|
def initialize(reopt, only_lv_consumers = false, max_num_lv_nodes, average_building_peak_catalog_path, lv_limit)
|
@@ -55,8 +55,10 @@ module URBANopt
|
|
55
55
|
@max_num_lv_nodes = max_num_lv_nodes
|
56
56
|
@customers = []
|
57
57
|
@customers_ext = []
|
58
|
+
@profile_date_time = []
|
58
59
|
@profile_customer_p = []
|
59
60
|
@profile_customer_q = []
|
61
|
+
@profile_date_time_ext = []
|
60
62
|
@profile_customer_p_ext = []
|
61
63
|
@profile_customer_q_ext = []
|
62
64
|
@power_factor = power_factor
|
@@ -73,8 +75,10 @@ module URBANopt
|
|
73
75
|
# while the 2nd option is run in case "only LV" set to false and the consumption for each building will be placed in a single node
|
74
76
|
def construct_consumer(profiles, single_values, building_map, building_nodes, height, users, folder)
|
75
77
|
if @only_lv_consumers
|
78
|
+
planning_date_time = []
|
76
79
|
planning_profile_node_active = []
|
77
80
|
planning_profile_node_reactive = []
|
81
|
+
yearly_date_time = []
|
78
82
|
yearly_profile_node_active = []
|
79
83
|
yearly_profile_node_reactive = []
|
80
84
|
nodes_per_bldg, area, medium_voltage = av_peak_cons_per_building_type(folder['building_types'])
|
@@ -106,19 +110,23 @@ module URBANopt
|
|
106
110
|
else
|
107
111
|
voltage_default, phases = voltage_values(peak_active_power_cons / @power_factor)
|
108
112
|
end
|
109
|
-
|
113
|
+
|
110
114
|
for k in 0..profiles[:planning_profile_cust_active].length - 1
|
115
|
+
planning_date_time[k]=profiles[:planning_date_time][k]
|
111
116
|
planning_profile_node_active[k] = ((profiles[:planning_profile_cust_active][k]) / nodes_per_bldg).round(2)
|
112
117
|
planning_profile_node_reactive[k] = ((profiles[:planning_profile_cust_reactive][k]) / nodes_per_bldg).round(2)
|
113
118
|
end
|
114
119
|
for k in 0..profiles[:yearly_profile_cust_active].length - 1
|
120
|
+
yearly_date_time[k]=profiles[:yearly_date_time][k]
|
115
121
|
yearly_profile_node_active[k] = ((profiles[:yearly_profile_cust_active][k]) / nodes_per_bldg).round(2)
|
116
122
|
yearly_profile_node_reactive[k] = ((profiles[:yearly_profile_cust_reactive][k]) / nodes_per_bldg).round(2)
|
117
123
|
end
|
118
124
|
@customers.push([coordinates, voltage_default, peak_active_power_cons, peak_reactive_power_cons, phases])
|
119
125
|
@customers_ext.push([coordinates, voltage_default, peak_active_power_cons, peak_reactive_power_cons, phases, area, height, (single_values[:energy] / nodes_per_bldg).round(2), peak_active_power_cons, peak_reactive_power_cons, users])
|
126
|
+
@profile_date_time.push([planning_date_time])
|
120
127
|
@profile_customer_q.push([id, 24, planning_profile_node_reactive])
|
121
128
|
@profile_customer_p.push([id, 24, planning_profile_node_active])
|
129
|
+
@profile_date_time_ext.push([yearly_date_time])
|
122
130
|
@profile_customer_p_ext.push([id, 8760, yearly_profile_node_active])
|
123
131
|
@profile_customer_q_ext.push([id, 8760, yearly_profile_node_reactive])
|
124
132
|
|
@@ -131,8 +139,10 @@ module URBANopt
|
|
131
139
|
voltage_default, phases = voltage_values(single_values[:peak_active_power_cons] / @power_factor * 0.9) # applying safety factor
|
132
140
|
@customers.push([building_map, voltage_default, single_values[:peak_active_power_cons], single_values[:peak_reactive_power_cons], phases])
|
133
141
|
@customers_ext.push([building_map, voltage_default, single_values[:peak_active_power_cons], single_values[:peak_reactive_power_cons], phases, area, height, (single_values[:energy]).round(2), single_values[:peak_active_power_cons], single_values[:peak_reactive_power_cons], users])
|
142
|
+
@profile_date_time.push([profiles[:planning_date_time]])
|
134
143
|
@profile_customer_q.push([id, 24, profiles[:planning_profile_cust_reactive]])
|
135
144
|
@profile_customer_p.push([id, 24, profiles[:planning_profile_cust_active]])
|
145
|
+
@profile_date_time_ext.push([profiles[:yearly_date_time]])
|
136
146
|
@profile_customer_p_ext.push([id, 8760, profiles[:yearly_profile_cust_active]])
|
137
147
|
@profile_customer_q_ext.push([id, 8760, profiles[:yearly_profile_cust_reactive]])
|
138
148
|
end
|
@@ -234,10 +244,12 @@ module URBANopt
|
|
234
244
|
# content = CSV.foreach(csv_feature_report, headers: true) do |power|
|
235
245
|
CSV.foreach(csv_feature_report, headers: true) do |power|
|
236
246
|
@power_factor = power['Electricity:Facility Power(kW)'].to_f / power['Electricity:Facility Apparent Power(kVA)'].to_f
|
247
|
+
profiles[:yearly_date_time].push(power['Datetime'])
|
237
248
|
profiles[:yearly_profile_cust_active].push(power['Electricity:Facility Power(kW)'].to_f)
|
238
249
|
profiles[:yearly_profile_cust_reactive].push(profiles[:yearly_profile_cust_active][k] * Math.tan(Math.acos(@power_factor)))
|
239
250
|
single_values[:energy] += power['REopt:Electricity:Load:Total(kw)'].to_f # calculating the yearly energy consumed by each feature
|
240
251
|
if k >= profile_start_max && k <= profile_start_max + hours
|
252
|
+
profiles[:planning_date_time].push(power['Datetime'])
|
241
253
|
profiles[:planning_profile_cust_active].push(power['Electricity:Facility Power(kW)'].to_f)
|
242
254
|
if power['Electricity:Facility Power(kW)'].to_f > single_values[:peak_active_power_cons]
|
243
255
|
single_values[:peak_active_power_cons] = power['Electricity:Facility Power(kW)'].to_f
|
@@ -258,6 +258,10 @@ module URBANopt
|
|
258
258
|
File.open(File.join(@run_dir, @rnm_dirname, 'customers_ext.txt'), 'w+') do |g|
|
259
259
|
g.puts(prosumers.customers_ext.map { |w| w.join(';') })
|
260
260
|
end
|
261
|
+
File.open(File.join(@run_dir, @rnm_dirname, 'timestamps.csv'), 'w+') do |g|
|
262
|
+
g.puts("Datetime\n")
|
263
|
+
g.puts(prosumers.profile_date_time[0].map { |w| w.join("\n") })
|
264
|
+
end
|
261
265
|
File.open(File.join(@run_dir, @rnm_dirname, 'cust_profile_p.txt'), 'w+') do |g|
|
262
266
|
g.puts(prosumers.profile_customer_p.map { |w| w.join(';') })
|
263
267
|
end
|
@@ -267,6 +271,10 @@ module URBANopt
|
|
267
271
|
# CSV.open(File.join(@run_dir, @rnm_dirname, "cust_profile_q_extendido.csv"), "w") do |csv|
|
268
272
|
# csv << [prosumers.profile_customer_q_ext]
|
269
273
|
# end
|
274
|
+
File.open(File.join(@run_dir, @rnm_dirname, 'timestamps_extendido.csv'), 'w+') do |g|
|
275
|
+
g.puts("Datetime\n")
|
276
|
+
g.puts(prosumers.profile_date_time_ext[0].map { |w| w.join("\n") })
|
277
|
+
end
|
270
278
|
File.open(File.join(@run_dir, @rnm_dirname, 'cust_profile_q_extendido.txt'), 'w+') do |g|
|
271
279
|
g.puts(prosumers.profile_customer_q_ext.map { |w| w.join(';') })
|
272
280
|
end
|
@@ -289,6 +297,7 @@ module URBANopt
|
|
289
297
|
File.open(File.join(@run_dir, @rnm_dirname, 'gen_profile_p_extendido.txt'), 'w+') do |g|
|
290
298
|
g.puts(prosumers.profile_dg_p_extended.map { |w| w.join(';') })
|
291
299
|
end
|
300
|
+
ficheros_entrada_inc.push('Timestamps;timestamps.csv;timestamps_extendido.csv')
|
292
301
|
ficheros_entrada_inc.push('CClienteGreenfield;customers_ext.txt;cust_profile_p.txt;cust_profile_q.txt;cust_profile_p_extendido.txt;cust_profile_q_extendido.txt')
|
293
302
|
ficheros_entrada_inc.push('CGeneradorGreenfield;generators.txt;gen_profile_p.txt;gen_profile_q.txt;gen_profile_p_extendido.txt;gen_profile_q_extendido.txt')
|
294
303
|
ficheros_entrada_inc.push('END')
|
@@ -302,18 +311,27 @@ module URBANopt
|
|
302
311
|
File.open(File.join(@run_dir, @rnm_dirname, 'customers_ext.txt'), 'w+') do |g|
|
303
312
|
g.puts(consumers.customers_ext.map { |w| w.join(';') })
|
304
313
|
end
|
314
|
+
File.open(File.join(@run_dir, @rnm_dirname, 'timestamps.csv'), 'w+') do |g|
|
315
|
+
g.puts("Datetime\n")
|
316
|
+
g.puts(consumers.profile_date_time[0].map { |w| w.join("\n") })
|
317
|
+
end
|
305
318
|
File.open(File.join(@run_dir, @rnm_dirname, 'cust_profile_p.txt'), 'w+') do |g|
|
306
319
|
g.puts(consumers.profile_customer_p.map { |w| w.join(';') })
|
307
320
|
end
|
308
321
|
File.open(File.join(@run_dir, @rnm_dirname, 'cust_profile_q.txt'), 'w+') do |g|
|
309
322
|
g.puts(consumers.profile_customer_q.map { |w| w.join(';') })
|
310
323
|
end
|
324
|
+
File.open(File.join(@run_dir, @rnm_dirname, 'timestamps_extendido.csv'), 'w+') do |g|
|
325
|
+
g.puts("Datetime\n")
|
326
|
+
g.puts(consumers.profile_date_time_ext[0].map { |w| w.join("\n") })
|
327
|
+
end
|
311
328
|
File.open(File.join(@run_dir, @rnm_dirname, 'cust_profile_q_extendido.txt'), 'w+') do |g|
|
312
329
|
g.puts(consumers.profile_customer_q_ext.map { |w| w.join(';') })
|
313
330
|
end
|
314
331
|
File.open(File.join(@run_dir, @rnm_dirname, 'cust_profile_p_extendido.txt'), 'w+') do |g|
|
315
332
|
g.puts(consumers.profile_customer_p_ext.map { |w| w.join(';') })
|
316
333
|
end
|
334
|
+
ficheros_entrada_inc.push('Timestamps;timestamps.csv;timestamps_extendido.csv')
|
317
335
|
ficheros_entrada_inc.push('CClienteGreenfield;customers_ext.txt;cust_profile_p.txt;cust_profile_q.txt;cust_profile_p_extendido.txt;cust_profile_q_extendido.txt')
|
318
336
|
ficheros_entrada_inc.push('END')
|
319
337
|
File.open(File.join(@run_dir, @rnm_dirname, 'ficheros_entrada_inc.txt'), 'w+') do |g|
|
@@ -45,7 +45,7 @@ module URBANopt
|
|
45
45
|
# creating a class that creates the consumers input required by the RNM-US model,
|
46
46
|
# according to their geographic location, energy consumption and peak demand, and power consumption profiles
|
47
47
|
class Prosumers
|
48
|
-
attr_accessor :customers, :customers_ext, :profile_customer_p, :profile_customer_q, :profile_customer_p_ext, :profile_customer_q_ext, :dg, :dg_profile_p, :dg_profile_q, :profile_dg_p_extended, :profile_dg_q_extended, :power_factor
|
48
|
+
attr_accessor :customers, :customers_ext, :profile_customer_p, :profile_customer_q, :profile_customer_p_ext, :profile_customer_q_ext, :dg, :dg_profile_p, :dg_profile_q, :profile_dg_p_extended, :profile_dg_q_extended, :power_factor, :profile_date_time, :profile_date_time_ext
|
49
49
|
|
50
50
|
# initializing all the attributes to build the inputs files required by the RNM-US model
|
51
51
|
def initialize(reopt, only_lv_consumers = false, max_num_lv_nodes, average_building_peak_catalog_path, lv_limit)
|
@@ -55,8 +55,10 @@ module URBANopt
|
|
55
55
|
@max_num_lv_nodes = max_num_lv_nodes
|
56
56
|
@customers = []
|
57
57
|
@customers_ext = []
|
58
|
+
@profile_date_time = []
|
58
59
|
@profile_customer_p = []
|
59
60
|
@profile_customer_q = []
|
61
|
+
@profile_date_time_ext = []
|
60
62
|
@profile_customer_p_ext = []
|
61
63
|
@profile_customer_q_ext = []
|
62
64
|
@dg = []
|
@@ -99,16 +101,20 @@ module URBANopt
|
|
99
101
|
end
|
100
102
|
@customers.push([building_map, id, voltage_default, single_values[:peak_active_power_cons], single_values[:peak_reactive_power_cons], phases])
|
101
103
|
@customers_ext.push([building_map, id, voltage_default, single_values[:peak_active_power_cons], single_values[:peak_reactive_power_cons], phases, area, height, (single_values[:energy]).round(2), single_values[:peak_active_power_cons], single_values[:peak_reactive_power_cons], users])
|
104
|
+
@profile_date_time.push([profiles_planning[:planning_date_time]])
|
102
105
|
@profile_customer_q.push([id, 48, profiles_planning[:planning_profile_cust_reactive]])
|
103
106
|
@profile_customer_p.push([id, 48, profiles_planning[:planning_profile_cust_active]])
|
107
|
+
@profile_date_time_ext.push([profiles[:yearly_date_time]])
|
104
108
|
@profile_customer_p_ext.push([id, 8760, profiles[:yearly_profile_cust_active]])
|
105
109
|
@profile_customer_q_ext.push([id, 8760, profiles[:yearly_profile_cust_reactive]])
|
106
110
|
|
107
111
|
if !der_capacity[:storage].nil? && der_capacity[:storage] > 0
|
108
112
|
@customers.push([building_map, id_batt, voltage_default, single_values[:peak_active_power_storage], single_values[:peak_reactive_power_storage], phases])
|
109
113
|
@customers_ext.push([building_map, id_batt, voltage_default, single_values[:peak_active_power_storage], single_values[:peak_reactive_power_storage], phases, area, height, (single_values[:energy]).round(2), single_values[:peak_active_power_storage], single_values[:peak_reactive_power_storage], users])
|
114
|
+
@profile_date_time.push([profiles_planning[:planning_date_time]])
|
110
115
|
@profile_customer_q.push([id_batt, 48, profiles_planning[:planning_profile_storage_reactive]])
|
111
116
|
@profile_customer_p.push([id_batt, 48, profiles_planning[:planning_profile_storage_active]])
|
117
|
+
@profile_date_time_ext.push([profiles[:yearly_date_time]])
|
112
118
|
@profile_customer_p_ext.push([id_batt, 8760, profiles[:yearly_profile_storage_active]])
|
113
119
|
@profile_customer_q_ext.push([id_batt, 8760, profiles[:yearly_profile_storage_reactive]])
|
114
120
|
end
|
@@ -129,8 +135,10 @@ module URBANopt
|
|
129
135
|
# among the nodes of each building
|
130
136
|
def construct_prosumer_lv(nodes_per_bldg = 0, profiles, profiles_planning, single_values, building_map, building_nodes, area, height, users, der_capacity)
|
131
137
|
# the default variables are defined (i.e. type and rurality type)
|
138
|
+
planning_date_time = []
|
132
139
|
planning_profile_node_active = []
|
133
140
|
planning_profile_node_reactive = []
|
141
|
+
yearly_date_time = []
|
134
142
|
yearly_profile_node_active = []
|
135
143
|
yearly_profile_node_reactive = []
|
136
144
|
closest_node = building_map[3].split('_')[1].to_i # refers to the closest node of the building in consideration to the street
|
@@ -159,17 +167,21 @@ module URBANopt
|
|
159
167
|
peak_reactive_power_cons = (single_values[:peak_reactive_power_cons] / nodes_consumers).round(2)
|
160
168
|
voltage_default, phases = voltage_values(peak_active_power_cons / @power_factor)
|
161
169
|
for k in 0..profiles_planning[:planning_profile_cust_active].length - 1
|
170
|
+
planning_date_time[k]=profiles_planning[:planning_date_time][k]
|
162
171
|
planning_profile_node_active[k] = (profiles_planning[:planning_profile_cust_active][k] / nodes_consumers).round(2)
|
163
172
|
planning_profile_node_reactive[k] = (profiles_planning[:planning_profile_cust_reactive][k] / nodes_consumers).round(2)
|
164
173
|
end
|
165
174
|
for k in 0..profiles[:yearly_profile_cust_active].length - 1
|
175
|
+
yearly_date_time[k]=profiles[:yearly_date_time][k]
|
166
176
|
yearly_profile_node_active[k] = (profiles[:yearly_profile_cust_active][k] / nodes_consumers).round(2)
|
167
177
|
yearly_profile_node_reactive[k] = (profiles[:yearly_profile_cust_reactive][k] / nodes_consumers).round(2)
|
168
178
|
end
|
169
179
|
@customers.push([coordinates, id, voltage_default, peak_active_power_cons, peak_reactive_power_cons, phases])
|
170
180
|
@customers_ext.push([coordinates, id, voltage_default, peak_active_power_cons, peak_reactive_power_cons, phases, area, height, (single_values[:energy] / nodes_consumers).round(2), peak_active_power_cons, peak_reactive_power_cons, users])
|
181
|
+
@profile_date_time.push([planning_date_time])
|
171
182
|
@profile_customer_q.push([id, 48, planning_profile_node_reactive])
|
172
183
|
@profile_customer_p.push([id, 48, planning_profile_node_active])
|
184
|
+
@profile_date_time_ext.push([yearly_date_time])
|
173
185
|
@profile_customer_p_ext.push([id, 8760, yearly_profile_node_active])
|
174
186
|
@profile_customer_q_ext.push([id, 8760, yearly_profile_node_reactive])
|
175
187
|
else
|
@@ -186,8 +198,10 @@ module URBANopt
|
|
186
198
|
if !der_capacity[:storage].nil? && der_capacity[:storage] > 0
|
187
199
|
@customers.push([coordinates, id_batt, voltage_default, single_values[:peak_active_power_storage], single_values[:peak_reactive_power_storage], phases])
|
188
200
|
@customers_ext.push([coordinates, id_batt, voltage_default, single_values[:peak_active_power_storage], single_values[:peak_reactive_power_storage], phases, area, height, (single_values[:energy]).round(2), single_values[:peak_active_power_storage], single_values[:peak_reactive_power_storage], users])
|
201
|
+
@profile_date_time.push([profiles[:planning_date_time]])
|
189
202
|
@profile_customer_q.push([id_batt, 48, profiles_planning[:planning_profile_storage_reactive]])
|
190
203
|
@profile_customer_p.push([id_batt, 48, profiles_planning[:planning_profile_storage_active]])
|
204
|
+
@profile_date_time_ext.push([profiles[:yearly_date_time]])
|
191
205
|
@profile_customer_p_ext.push([id_batt, 8760, profiles[:yearly_profile_storage_active]])
|
192
206
|
@profile_customer_q_ext.push([id_batt, 8760, profiles[:yearly_profile_storage_reactive]])
|
193
207
|
end
|
@@ -285,6 +299,7 @@ module URBANopt
|
|
285
299
|
|
286
300
|
# method to order profiles consistently
|
287
301
|
def profiles_planning_creation(profiles_planning, power, single_values, i, hours, power_factor)
|
302
|
+
profiles_planning[:planning_date_time][i]=power['Datetime']
|
288
303
|
profiles_planning[:planning_profile_cust_active][i] = power['REopt:Electricity:Load:Total(kw)'].to_f
|
289
304
|
profiles_planning[:planning_profile_storage_active][i] = power['REopt:Electricity:Grid:ToBattery(kw)'].to_f + power['REopt:ElectricityProduced:Generator:ToBattery(kw)'].to_f + power['REopt:ElectricityProduced:PV:ToBattery(kw)'].to_f + power['REopt:ElectricityProduced:Wind:ToBattery(kw)'].to_f - power['REopt:Electricity:Storage:ToLoad(kw)'].to_f - power['REopt:Electricity:Storage:ToGrid(kw)'].to_f
|
290
305
|
profiles_planning[:planning_profile_dg_active][i] = power['REopt:ElectricityProduced:Total(kw)'].to_f
|
@@ -341,6 +356,7 @@ module URBANopt
|
|
341
356
|
max_peak = 0
|
342
357
|
CSV.foreach(csv_feature_report, headers: true) do |power|
|
343
358
|
@power_factor = power['Electricity:Facility Power(kW)'].to_f / power['Electricity:Facility Apparent Power(kVA)'].to_f
|
359
|
+
profiles[:yearly_date_time].push(power['Datetime'])
|
344
360
|
profiles[:yearly_profile_cust_active].push(power['REopt:Electricity:Load:Total(kw)'].to_f)
|
345
361
|
profiles[:yearly_profile_cust_reactive].push(profiles[:yearly_profile_cust_active][k] * Math.tan(Math.acos(@power_factor)))
|
346
362
|
profiles[:yearly_profile_dg_active].push(power['REopt:ElectricityProduced:Total(kw)'].to_f)
|
@@ -7,45 +7,147 @@ import math
|
|
7
7
|
import networkx as nx
|
8
8
|
import opendss_interface
|
9
9
|
import plot_lib
|
10
|
+
import report
|
11
|
+
import os
|
12
|
+
|
10
13
|
|
11
14
|
class Validation:
|
12
|
-
def __init__(self, folder):
|
13
|
-
|
14
|
-
|
15
|
+
def __init__(self, folder,sb_numeric_ids):
|
16
|
+
"""Initialices the folder variables"""
|
17
|
+
self.main_folder = folder #Main uppper level folder (needed to search for OpenDSS files)
|
18
|
+
self.folder=folder+'/Validation' #Folder where the Validation results are saved
|
19
|
+
self.b_numeric_ids=sb_numeric_ids.lower()=='true' #Use numeric IDs in the hierarchical plot of the network
|
20
|
+
self.mkdir(self.folder) #It creates the validation folder if it does not exist
|
21
|
+
|
22
|
+
def mkdir(self,directory):
|
23
|
+
"""Checkes whether the folder exists, if not it creates it"""
|
24
|
+
if (not os.path.exists(directory)):
|
25
|
+
os.mkdir(directory)
|
26
|
+
|
27
|
+
def make_dirs(self):
|
28
|
+
"""Creates all the subfolder, it they don't exist"""
|
29
|
+
path=self.folder+'/'
|
30
|
+
self.mkdir(path+'Voltage')
|
31
|
+
self.mkdir(path+'Voltage/CSV')
|
32
|
+
self.mkdir(path+'Voltage/Figures')
|
33
|
+
self.mkdir(path+'Unbalance')
|
34
|
+
self.mkdir(path+'Unbalance/CSV')
|
35
|
+
self.mkdir(path+'Unbalance/Figures')
|
36
|
+
self.mkdir(path+'Loading')
|
37
|
+
self.mkdir(path+'Loading/CSV')
|
38
|
+
self.mkdir(path+'Loading/Figures')
|
39
|
+
self.mkdir(path+'Losses')
|
40
|
+
self.mkdir(path+'Losses/CSV')
|
41
|
+
self.mkdir(path+'Losses/Figures')
|
42
|
+
self.mkdir(path+'Loads')
|
43
|
+
self.mkdir(path+'Loads/Figures')
|
44
|
+
self.mkdir(path+'Equipment')
|
45
|
+
self.mkdir(path+'Equipment/Figures')
|
46
|
+
self.mkdir(path+'Network')
|
47
|
+
self.mkdir(path+'Network/Figures')
|
48
|
+
self.mkdir(path+'Summary')
|
49
|
+
|
50
|
+
def define_ranges(self):
|
51
|
+
"""Defines the allowed ranges (to determine violations), the limits (to plot the lines in the plots), and the display ranges (for the axis in the figures)"""
|
52
|
+
#Voltages
|
53
|
+
v_range_voltage={}
|
54
|
+
v_range_voltage['allowed_range']=(0.95, 1.05)
|
55
|
+
v_range_voltage['limits']=[0.95, 1.05]
|
56
|
+
v_range_voltage['display_range']=(0.85, 1.15)
|
57
|
+
#Lodaing
|
58
|
+
v_range_loading={}
|
59
|
+
v_range_loading['allowed_range']=(0, 1)
|
60
|
+
v_range_loading['limits']=[1]
|
61
|
+
v_range_loading['display_range']=(0, 1.3)
|
62
|
+
#Unbalance
|
63
|
+
v_range_unbalance={}
|
64
|
+
v_range_unbalance['allowed_range']=(0,0.02)
|
65
|
+
v_range_unbalance['limits']=[0.02]
|
66
|
+
v_range_unbalance['display_range']=(0,0.021)
|
67
|
+
#Show all variables option
|
68
|
+
v_range_show_all={}
|
69
|
+
v_range_show_all['allowed_range']=()
|
70
|
+
v_range_show_all['limits']=[]
|
71
|
+
v_range_show_all['display_range']=()
|
72
|
+
|
73
|
+
return v_range_voltage,v_range_loading,v_range_unbalance,v_range_show_all
|
74
|
+
|
75
|
+
|
15
76
|
def main_validation(self):
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
77
|
+
"""Carries out the whole validation of the distribution system"""
|
78
|
+
#Path and file name
|
79
|
+
master_file_full_path = self.main_folder + '/dss_files/' + 'Master.dss' #Path for the Master file
|
80
|
+
#Number of periods (always 12 months)
|
81
|
+
num_periods=12
|
82
|
+
#Start and end index
|
83
|
+
#Define only to simulate only a subset of hours
|
84
|
+
#Set to None to use all timestamps
|
85
|
+
start_index = None #Default initial index (0/None)
|
86
|
+
end_index = None #Default final index, 1 year, 8760h (8760/None)
|
25
87
|
|
26
|
-
#
|
27
|
-
|
28
|
-
#
|
88
|
+
#Create sub-folders
|
89
|
+
self.make_dirs()
|
90
|
+
#Define the ranges of voltage, loading, unbalance and a default range to show all the values
|
91
|
+
v_range_voltage,v_range_loading,v_range_unbalance,v_range_show_all=self.define_ranges()
|
29
92
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
edges=myopendss_io.get_edges()
|
38
|
-
myplot_lib=plot_lib.Plot_Lib(folder)
|
39
|
-
myplot_lib.plot_hist('Voltage',v_voltage_yearly,v_voltage_period,v_range_voltage,40,num_periods,v_limits_voltage)
|
40
|
-
myplot_lib.plot_hist('Loading',v_loading_yearly,v_loading_period,v_range_loading,80,num_periods,v_limits_loading)
|
41
|
-
myplot_lib.plot_losses(v_subs_losses_yearly,v_line_losses_yearly)
|
42
|
-
myplot_lib.plot_graph(edges,v_dict_voltage,v_range_voltage,v_dict_loading,v_range_loading,dict_buses_element)
|
93
|
+
#For tests
|
94
|
+
#start_index = 2000 #Default initial index
|
95
|
+
#end_index = 4000 #Simulate few hours (still broken down in 12 periods/months)
|
96
|
+
#v_range_voltage['allowed_range']=(0.975, 1.025) #More stringent limmits than the standard ones to show violations
|
97
|
+
#v_range_loading['allowed_range']=(0,0.3)
|
98
|
+
#v_range_unbalance['allowed_range']=(0, 4e-5)
|
99
|
+
#v_range_unbalance['allowed_range']=(0, 2.1e-5)
|
43
100
|
|
101
|
+
#Run power flow iteratively and obtain the results
|
102
|
+
myopendss_io=opendss_interface.OpenDSS_Interface(folder,self.b_numeric_ids)
|
103
|
+
v_dict_buses_ids,v_dict_ids_buses,v_dict_voltage,v_voltage_yearly,v_voltage_period,v_power_yearly,v_power_period,v_dict_loading,v_loading_yearly,v_loading_period,v_dict_losses,v_subs_losses_yearly,v_line_losses_yearly,dict_buses_element,v_dict_loads,v_loads_kw_yearly,v_loads_kw_period,v_loads_kvar_yearly,v_loads_kvar_period,v_total_load_kw_yearly,v_total_load_kvar_yearly, v_loads_kw, v_loads_kvar, v_dict_unbalance,v_unbalance_yearly,v_unbalance_period,dict_lines,v_lines_norm_amps,dict_transformers,v_transformers_kva,timestamps,v_months,v_hours=myopendss_io.solve_powerflow_iteratively(num_periods,start_index,end_index,master_file_full_path,v_range_voltage,v_range_loading,v_range_unbalance)
|
44
104
|
|
105
|
+
#Save voltage, unbalance, loading and losses results in CSV files
|
106
|
+
myopendss_io.write_dict('Voltage/CSV',v_dict_voltage,v_range_show_all,'Voltages (p.u.)','Buses',v_dict_buses_ids,timestamps,v_hours)
|
107
|
+
myopendss_io.write_dict('Voltage/CSV',v_dict_voltage,v_range_voltage,'Voltage Violations (p.u.)','Buses',v_dict_buses_ids,timestamps,v_hours)
|
108
|
+
myopendss_io.write_dict('Unbalance/CSV',v_dict_unbalance,v_range_show_all,'Unbalance (p.u.)','Buses',v_dict_buses_ids,timestamps,v_hours)
|
109
|
+
myopendss_io.write_dict('Unbalance/CSV',v_dict_unbalance,v_range_unbalance,'Unbalance Violations (p.u.)','Buses',v_dict_buses_ids,timestamps,v_hours)
|
110
|
+
myopendss_io.write_dict('Loading/CSV',v_dict_loading,v_range_show_all,'Loading (p.u.)','Branches',None,timestamps,v_hours)
|
111
|
+
myopendss_io.write_dict('Loading/CSV',v_dict_loading,v_range_loading,'Loading Violations (p.u.)','Branches',None,timestamps,v_hours)
|
112
|
+
myopendss_io.write_dict('losses/CSV',v_dict_losses,v_range_show_all,'Losses','Branches',None,timestamps,v_hours)
|
113
|
+
if self.b_numeric_ids:
|
114
|
+
myopendss_io.write_id_dict('Network/Figures','IDs_Buses',v_dict_buses_ids)
|
115
|
+
#Get the edges of the network (for later making a hierarchical representation of the network)
|
116
|
+
closed_edges,open_edges=myopendss_io.get_edges(v_dict_buses_ids)
|
117
|
+
#Plot all the figures
|
118
|
+
myplot_lib=plot_lib.Plot_Lib(folder,self.b_numeric_ids)
|
119
|
+
#Voltage
|
120
|
+
myplot_lib.plot_hist('Voltage','Voltage (p.u.)',v_voltage_yearly,v_voltage_period,v_range_voltage,num_periods,40,v_months)
|
121
|
+
myplot_lib.plot_violin_monthly('Voltage/Figures','Voltage (p.u.)',v_voltage_yearly,v_voltage_period,v_range_voltage,num_periods,v_months)
|
122
|
+
#Unbalance
|
123
|
+
myplot_lib.plot_hist('Unbalance','Unbalance (p.u.)',v_unbalance_yearly,v_unbalance_period,v_range_unbalance,num_periods,40,v_months)
|
124
|
+
myplot_lib.plot_violin_monthly('Unbalance/Figures','Unbalance (p.u.)',v_unbalance_yearly,v_unbalance_period,v_range_unbalance,num_periods,v_months)
|
125
|
+
#Loading
|
126
|
+
myplot_lib.plot_hist('Loading','Loading (p.u.)',v_loading_yearly,v_loading_period,v_range_loading,num_periods,80,v_months)
|
127
|
+
myplot_lib.plot_violin_monthly('Loading/Figures','Loading (p.u.)',v_loading_yearly,v_loading_period,v_range_loading,num_periods,v_months)
|
128
|
+
#Loads and load shpaes
|
129
|
+
myplot_lib.plot_duration_curve('Loads/Figures',v_total_load_kw_yearly,v_total_load_kvar_yearly,False,v_hours)
|
130
|
+
myplot_lib.plot_violin_monthly_two_vars('Loads/Figures','Loads',v_loads_kw_yearly,v_loads_kw_period,v_loads_kvar_yearly,v_loads_kvar_period,v_range_show_all,num_periods,v_months)
|
131
|
+
myplot_lib.plot_violin('Loads/Figures','Loads Peak (kW)',v_loads_kw,v_range_show_all)
|
132
|
+
#Losses
|
133
|
+
myplot_lib.plot_duration_curve('Losses/Figures',v_subs_losses_yearly,v_line_losses_yearly,True,v_hours)
|
134
|
+
#Equipment parameters
|
135
|
+
myplot_lib.plot_violin('Equipment/Figures','Power Line - Normal Amps (A)',v_lines_norm_amps,v_range_show_all)
|
136
|
+
myplot_lib.plot_violin('Equipment/Figures','Transformer (kVA)',v_transformers_kva,v_range_show_all)
|
137
|
+
#Summary operational report
|
138
|
+
myreport=report.Report(folder,self.b_numeric_ids)
|
139
|
+
myreport.write_summary_operational_report('Summary',v_dict_voltage,v_range_voltage,v_dict_unbalance,v_range_unbalance,v_dict_loading,v_range_loading,v_dict_loads)
|
140
|
+
#Hierarchical representation of the network
|
141
|
+
myplot_lib.plot_graph('Network/Figures',closed_edges,open_edges,v_dict_voltage,v_range_voltage,v_dict_loading,v_range_loading,dict_buses_element,v_dict_buses_ids,v_dict_ids_buses)
|
142
|
+
|
45
143
|
|
144
|
+
#def main_validation(folder): #Example: uncomment to make the script run from a function
|
145
|
+
#folder = './files/'
|
46
146
|
if __name__ == "__main__":
|
47
|
-
|
48
|
-
#python main_validation.py files
|
49
|
-
folder = sys.argv[1]
|
50
|
-
|
51
|
-
valid
|
147
|
+
"""Runs direclty as a script if called from the command window"""
|
148
|
+
#Example of use: python main_validation.py files
|
149
|
+
folder = sys.argv[1] #Use the folder specified in the arguments
|
150
|
+
sb_numeric_ids = sys.argv[2] #Use numeric IDs in the hierarchical plot of the network
|
151
|
+
valid=Validation(folder,sb_numeric_ids)
|
152
|
+
valid.main_validation() #Call the main validation function
|
153
|
+
|