openstudio-ee 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/Rakefile +2 -0
  4. data/lib/measures/ImproveFanTotalEfficiencybyPercentage/measure.rb +333 -0
  5. data/lib/measures/ImproveFanTotalEfficiencybyPercentage/measure.xml +150 -0
  6. data/lib/measures/ReplaceFanTotalEfficiency/measure.rb +330 -0
  7. data/lib/measures/ReplaceFanTotalEfficiency/measure.xml +150 -0
  8. data/lib/measures/add_apszhp_to_each_zone/measure.rb +607 -0
  9. data/lib/measures/add_apszhp_to_each_zone/measure.xml +184 -0
  10. data/lib/measures/add_energy_recovery_ventilator/measure.rb +354 -0
  11. data/lib/measures/add_energy_recovery_ventilator/measure.xml +78 -0
  12. data/lib/measures/improve_simple_glazing_by_percentage/measure.rb +81 -0
  13. data/lib/measures/improve_simple_glazing_by_percentage/measure.xml +70 -0
  14. data/lib/measures/reduce_water_use_by_percentage/measure.rb +61 -0
  15. data/lib/measures/reduce_water_use_by_percentage/measure.xml +62 -0
  16. data/lib/measures/replace_hvac_with_gshp_and_doas/measure.rb +511 -0
  17. data/lib/measures/replace_hvac_with_gshp_and_doas/measure.xml +375 -0
  18. data/lib/measures/replace_hvac_with_gshp_and_doas/resources/OsLib_AedgMeasures.rb +454 -0
  19. data/lib/measures/replace_hvac_with_gshp_and_doas/resources/OsLib_Constructions.rb +221 -0
  20. data/lib/measures/replace_hvac_with_gshp_and_doas/resources/OsLib_Geometry.rb +41 -0
  21. data/lib/measures/replace_hvac_with_gshp_and_doas/resources/OsLib_HVAC.rb +1682 -0
  22. data/lib/measures/replace_hvac_with_gshp_and_doas/resources/OsLib_HelperMethods.rb +114 -0
  23. data/lib/measures/replace_hvac_with_gshp_and_doas/resources/OsLib_LightingAndEquipment.rb +99 -0
  24. data/lib/measures/replace_hvac_with_gshp_and_doas/resources/OsLib_Schedules.rb +142 -0
  25. data/lib/measures/replace_simple_glazing/measure.rb +86 -0
  26. data/lib/measures/replace_simple_glazing/measure.xml +78 -0
  27. data/lib/measures/set_boiler_thermal_efficiency/measure.rb +520 -0
  28. data/lib/measures/set_boiler_thermal_efficiency/measure.xml +78 -0
  29. data/lib/measures/set_water_heater_efficiency_heat_lossand_peak_water_flow_rate/measure.rb +207 -0
  30. data/lib/measures/set_water_heater_efficiency_heat_lossand_peak_water_flow_rate/measure.xml +78 -0
  31. data/lib/measures/tenant_star_internal_loads/measure.rb +134 -0
  32. data/lib/measures/tenant_star_internal_loads/measure.xml +67 -0
  33. data/lib/measures/tenant_star_internal_loads/resources/os_lib_helper_methods.rb +401 -0
  34. data/lib/measures/vr_fwith_doas/measure.rb +468 -0
  35. data/lib/measures/vr_fwith_doas/measure.xml +298 -0
  36. data/lib/measures/vr_fwith_doas/resources/OsLib_AedgMeasures.rb +454 -0
  37. data/lib/measures/vr_fwith_doas/resources/OsLib_Constructions.rb +221 -0
  38. data/lib/measures/vr_fwith_doas/resources/OsLib_Geometry.rb +41 -0
  39. data/lib/measures/vr_fwith_doas/resources/OsLib_HVAC.rb +1516 -0
  40. data/lib/measures/vr_fwith_doas/resources/OsLib_HelperMethods.rb +114 -0
  41. data/lib/measures/vr_fwith_doas/resources/OsLib_LightingAndEquipment.rb +99 -0
  42. data/lib/measures/vr_fwith_doas/resources/OsLib_Schedules.rb +142 -0
  43. data/lib/openstudio/ee_measures/version.rb +1 -1
  44. data/openstudio-ee.gemspec +7 -5
  45. metadata +48 -9
@@ -0,0 +1,67 @@
1
+ <measure>
2
+ <schema_version>3.0</schema_version>
3
+ <name>tenant_star_internal_loads</name>
4
+ <uid>8c10b18d-c66d-43dc-9ef5-b82dfed0edde</uid>
5
+ <version_id>6c19271d-8f07-4983-a219-810c8fa75da5</version_id>
6
+ <version_modified>20180717T215712Z</version_modified>
7
+ <xml_checksum>2C38F48B</xml_checksum>
8
+ <class_name>TenantStarInternalLoads</class_name>
9
+ <display_name>Tenant Star Internal Loads</display_name>
10
+ <description>Overrides existing model values for lightings, equipment, people, and infiltration.</description>
11
+ <modeler_description>Lighting should be stacked value unless we add uncertainty. Equipment and people will vary based on information provided by tenant, and infiltration will be used for uncertainty. Schedules will be addressed in a separate measure that creates parametric schedules based on hours of operation.</modeler_description>
12
+ <arguments>
13
+ <argument>
14
+ <name>epd</name>
15
+ <display_name>Electric Equipment Power Density</display_name>
16
+ <description>Electric Power Density including servers.</description>
17
+ <type>Double</type>
18
+ <units>W/ft^2</units>
19
+ <required>true</required>
20
+ <model_dependent>false</model_dependent>
21
+ <default_value>0.55</default_value>
22
+ </argument>
23
+ </arguments>
24
+ <outputs/>
25
+ <provenances/>
26
+ <tags>
27
+ <tag>Whole Building.Space Types</tag>
28
+ </tags>
29
+ <attributes>
30
+ <attribute>
31
+ <name>Measure Type</name>
32
+ <value>ModelMeasure</value>
33
+ <datatype>string</datatype>
34
+ </attribute>
35
+ </attributes>
36
+ <files>
37
+ <file>
38
+ <filename>example_model.osm</filename>
39
+ <filetype>osm</filetype>
40
+ <usage_type>test</usage_type>
41
+ <checksum>53D14E69</checksum>
42
+ </file>
43
+ <file>
44
+ <filename>tenant_star_internal_loads_test.rb</filename>
45
+ <filetype>rb</filetype>
46
+ <usage_type>test</usage_type>
47
+ <checksum>877E59E4</checksum>
48
+ </file>
49
+ <file>
50
+ <filename>os_lib_helper_methods.rb</filename>
51
+ <filetype>rb</filetype>
52
+ <usage_type>resource</usage_type>
53
+ <checksum>9CFC43FB</checksum>
54
+ </file>
55
+ <file>
56
+ <version>
57
+ <software_program>OpenStudio</software_program>
58
+ <identifier>2.1.0</identifier>
59
+ <min_compatible>2.1.0</min_compatible>
60
+ </version>
61
+ <filename>measure.rb</filename>
62
+ <filetype>rb</filetype>
63
+ <usage_type>script</usage_type>
64
+ <checksum>F92433BA</checksum>
65
+ </file>
66
+ </files>
67
+ </measure>
@@ -0,0 +1,401 @@
1
+ # frozen_string_literal: true
2
+
3
+ # *******************************************************************************
4
+ # OpenStudio(R), Copyright (c) 2008-2018, Alliance for Sustainable Energy, LLC.
5
+ # All rights reserved.
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # (1) Redistributions of source code must retain the above copyright notice,
10
+ # this list of conditions and the following disclaimer.
11
+ #
12
+ # (2) Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # (3) Neither the name of the copyright holder nor the names of any contributors
17
+ # may be used to endorse or promote products derived from this software without
18
+ # specific prior written permission from the respective party.
19
+ #
20
+ # (4) Other than as required in clauses (1) and (2), distributions in any form
21
+ # of modifications or other derivative works may not use the "OpenStudio"
22
+ # trademark, "OS", "os", or any other confusingly similar designation without
23
+ # specific prior written permission from Alliance for Sustainable Energy, LLC.
24
+ #
25
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS
26
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
27
+ # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
28
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE
29
+ # UNITED STATES GOVERNMENT, OR THE UNITED STATES DEPARTMENT OF ENERGY, NOR ANY OF
30
+ # THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
31
+ # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
32
+ # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
33
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
34
+ # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
35
+ # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36
+ # *******************************************************************************
37
+
38
+ module OsLib_HelperMethods
39
+ # populate choice argument from model objects
40
+ def self.populateChoiceArgFromModelObjects(model, modelObject_args_hash, includeBuilding = nil)
41
+ # populate choice argument for constructions that are applied to surfaces in the model
42
+ modelObject_handles = OpenStudio::StringVector.new
43
+ modelObject_display_names = OpenStudio::StringVector.new
44
+
45
+ # looping through sorted hash of constructions
46
+ modelObject_args_hash.sort.map do |key, value|
47
+ modelObject_handles << value.handle.to_s
48
+ modelObject_display_names << key
49
+ end
50
+
51
+ unless includeBuilding.nil?
52
+ # add building to string vector with space type
53
+ building = model.getBuilding
54
+ modelObject_handles << building.handle.to_s
55
+ modelObject_display_names << includeBuilding
56
+ end
57
+
58
+ result = { 'modelObject_handles' => modelObject_handles, 'modelObject_display_names' => modelObject_display_names }
59
+ return result
60
+ end
61
+
62
+ # create variables in run from user arguments
63
+ def self.createRunVariables(runner, model, user_arguments, arguments)
64
+ result = {}
65
+
66
+ error = false
67
+ # use the built-in error checking
68
+ unless runner.validateUserArguments(arguments, user_arguments)
69
+ error = true
70
+ runner.registerError('Invalid argument values.')
71
+ end
72
+
73
+ user_arguments.each do |argument|
74
+ # get argument info
75
+ arg = user_arguments[argument]
76
+ arg_type = arg.print.lines($/)[1]
77
+
78
+ # create argument variable
79
+ if arg_type.include? 'Double, Required'
80
+ eval("result[\"#{arg.name}\"] = runner.getDoubleArgumentValue(\"#{arg.name}\", user_arguments)")
81
+ elsif arg_type.include? 'Integer, Required'
82
+ eval("result[\"#{arg.name}\"] = runner.getIntegerArgumentValue(\"#{arg.name}\", user_arguments)")
83
+ elsif arg_type.include? 'String, Required'
84
+ eval("result[\"#{arg.name}\"] = runner.getStringArgumentValue(\"#{arg.name}\", user_arguments)")
85
+ elsif arg_type.include? 'Boolean, Required'
86
+ eval("result[\"#{arg.name}\"] = runner.getBoolArgumentValue(\"#{arg.name}\", user_arguments)")
87
+ elsif arg_type.include? 'Choice, Required'
88
+ eval("result[\"#{arg.name}\"] = runner.getStringArgumentValue(\"#{arg.name}\", user_arguments)")
89
+ else
90
+ puts 'not setup to handle all argument types yet, or any optional arguments'
91
+ end
92
+ end
93
+
94
+ if error
95
+ return false
96
+ else
97
+ return result
98
+ end
99
+ end
100
+
101
+ # check choice argument made from model objects
102
+ def self.checkChoiceArgFromModelObjects(object, variableName, to_ObjectType, runner, user_arguments)
103
+ apply_to_building = false
104
+ modelObject = nil
105
+ if object.empty?
106
+ handle = runner.getStringArgumentValue(variableName, user_arguments)
107
+ if handle.empty?
108
+ runner.registerError("No #{variableName} was chosen.") # this logic makes this not work on an optional model object argument
109
+ else
110
+ runner.registerError("The selected #{variableName} with handle '#{handle}' was not found in the model. It may have been removed by another measure.")
111
+ end
112
+ return false
113
+ else
114
+ if !eval("object.get.#{to_ObjectType}").empty?
115
+ modelObject = eval("object.get.#{to_ObjectType}").get
116
+ elsif !object.get.to_Building.empty?
117
+ apply_to_building = true
118
+ else
119
+ runner.registerError("Script Error - argument not showing up as #{variableName}.")
120
+ return false
121
+ end
122
+ end
123
+
124
+ result = { 'modelObject' => modelObject, 'apply_to_building' => apply_to_building }
125
+ end
126
+
127
+ # check choice argument made from model objects
128
+ def self.checkOptionalChoiceArgFromModelObjects(object, variableName, to_ObjectType, runner, user_arguments)
129
+ apply_to_building = false
130
+ modelObject = nil
131
+ if object.empty?
132
+ handle = runner.getOptionalStringArgumentValue(variableName, user_arguments)
133
+ if handle.empty?
134
+ # do nothing, this is a valid option
135
+ puts 'hello'
136
+ modelObject = nil
137
+ apply_to_building = false
138
+ else
139
+ runner.registerError("The selected #{variableName} with handle '#{handle}' was not found in the model. It may have been removed by another measure.")
140
+ return false
141
+ end
142
+ else
143
+ if !eval("object.get.#{to_ObjectType}").empty?
144
+ modelObject = eval("object.get.#{to_ObjectType}").get
145
+ elsif !object.get.to_Building.empty?
146
+ apply_to_building = true
147
+ else
148
+ runner.registerError("Script Error - argument not showing up as #{variableName}.")
149
+ return false
150
+ end
151
+ end
152
+
153
+ result = { 'modelObject' => modelObject, 'apply_to_building' => apply_to_building }
154
+ end
155
+
156
+ # check value of double arguments
157
+ def self.checkDoubleAndIntegerArguments(runner, user_arguments, arg_check_hash)
158
+ error = false
159
+
160
+ # get hash values
161
+ min = arg_check_hash['min']
162
+ max = arg_check_hash['max']
163
+ min_eq_bool = arg_check_hash['min_eq_bool']
164
+ max_eq_bool = arg_check_hash['max_eq_bool']
165
+
166
+ arg_check_hash['arg_array'].each do |argument|
167
+ argument = user_arguments[argument]
168
+
169
+ # get arg values
170
+ arg_value = nil
171
+ if argument.hasValue
172
+ arg_value = argument.valueDisplayName.to_f # instead of valueAsDouble so it allows integer arguments as well
173
+ elsif argument.hasDefaultValue
174
+ arg_value = argument.defaultValueDisplayName.to_f
175
+ end
176
+ arg_display = argument.displayName
177
+
178
+ unless min.nil?
179
+ if min_eq_bool
180
+ if arg_value < min
181
+ runner.registerError("Please enter value greater than or equal to #{min} for #{arg_display}.") # add in argument display name
182
+ error = true
183
+ end
184
+ else
185
+ if arg_value <= min
186
+ runner.registerError("Please enter value greater than #{min} for #{arg_display}.") # add in argument display name
187
+ error = true
188
+ end
189
+ end
190
+ end
191
+ unless max.nil?
192
+ if max_eq_bool
193
+ if arg_value > max
194
+ runner.registerError("Please enter value less than or equal to #{max} for #{arg_display}.") # add in argument display name
195
+ error = true
196
+ end
197
+ else
198
+ if arg_value >= max
199
+ runner.registerError("Please enter value less than #{max} for #{arg_display}.") # add in argument display name
200
+ error = true
201
+ end
202
+ end
203
+ end
204
+ end
205
+
206
+ # check for any errors
207
+ if error
208
+ return false
209
+ else
210
+ return true
211
+ end
212
+ end
213
+
214
+ # open channel to log info/warning/error messages
215
+ def self.setup_log_msgs(runner, debug = false)
216
+ # Open a channel to log info/warning/error messages
217
+ @msg_log = OpenStudio::StringStreamLogSink.new
218
+ if debug
219
+ @msg_log.setLogLevel(OpenStudio::Debug)
220
+ else
221
+ @msg_log.setLogLevel(OpenStudio::Info)
222
+ end
223
+ @start_time = Time.new
224
+ @runner = runner
225
+ end
226
+
227
+ # Get all the log messages and put into output
228
+ # for users to see.
229
+ def self.log_msgs
230
+ @msg_log.logMessages.each do |msg|
231
+ # DLM: you can filter on log channel here for now
232
+ if /openstudio.*/.match?(msg.logChannel) # /openstudio\.model\..*/
233
+ # Skip certain messages that are irrelevant/misleading
234
+ next if msg.logMessage.include?('Skipping layer') || # Annoying/bogus "Skipping layer" warnings
235
+ msg.logChannel.include?('runmanager') || # RunManager messages
236
+ msg.logChannel.include?('setFileExtension') || # .ddy extension unexpected
237
+ msg.logChannel.include?('Translator') || # Forward translator and geometry translator
238
+ msg.logMessage.include?('UseWeatherFile') # 'UseWeatherFile' is not yet a supported option for YearDescription
239
+
240
+ # Report the message in the correct way
241
+ if msg.logLevel == OpenStudio::Info
242
+ @runner.registerInfo(msg.logMessage)
243
+ elsif msg.logLevel == OpenStudio::Warn
244
+ @runner.registerWarning("[#{msg.logChannel}] #{msg.logMessage}")
245
+ elsif msg.logLevel == OpenStudio::Error
246
+ @runner.registerError("[#{msg.logChannel}] #{msg.logMessage}")
247
+ elsif msg.logLevel == OpenStudio::Debug && @debug
248
+ @runner.registerInfo("DEBUG - #{msg.logMessage}")
249
+ end
250
+ end
251
+ end
252
+ @runner.registerInfo("Total Time = #{(Time.new - @start_time).round}sec.")
253
+ end
254
+
255
+ def self.check_upstream_measure_for_arg(runner, arg_name)
256
+ # 2.x methods (currently setup for measure display name but snake_case arg names)
257
+ arg_name_value = {}
258
+ runner.workflow.workflowSteps.each do |step|
259
+ if step.to_MeasureStep.is_initialized
260
+ measure_step = step.to_MeasureStep.get
261
+
262
+ measure_name = measure_step.measureDirName
263
+ if measure_step.name.is_initialized
264
+ measure_name = measure_step.name.get # this is instance name in PAT
265
+ end
266
+ if measure_step.result.is_initialized
267
+ result = measure_step.result.get
268
+ result.stepValues.each do |arg|
269
+ name = arg.name
270
+ value = arg.valueAsVariant.to_s
271
+ if name == arg_name
272
+ arg_name_value[:value] = value
273
+ arg_name_value[:measure_name] = measure_name
274
+ return arg_name_value # stop after find first one
275
+ end
276
+ end
277
+ else
278
+ # puts "No result for #{measure_name}"
279
+ end
280
+ else
281
+ # puts "This step is not a measure"
282
+ end
283
+ end
284
+
285
+ return arg_name_value
286
+ end
287
+
288
+ # populate choice argument from model objects. areaType should be string like "floorArea" or "exteriorArea"
289
+ # note: it seems like spaceType.floorArea does account for multiplier, so I don't have to call this method unless I have a custom collection of spaces.
290
+ def self.getAreaOfSpacesInArray(model, spaceArray, areaType = 'floorArea')
291
+ # find selected floor spaces, make array and get floor area.
292
+ totalArea = 0
293
+ spaceAreaHash = {}
294
+ spaceArray.each do |space|
295
+ spaceArea = eval("space.#{areaType}*space.multiplier")
296
+ spaceAreaHash[space] = spaceArea
297
+ totalArea += spaceArea
298
+ end
299
+
300
+ result = { 'totalArea' => totalArea, 'spaceAreaHash' => spaceAreaHash }
301
+ return result
302
+ end
303
+
304
+ # runs conversion and neat string, and returns value with units in string, optionally before or after the value
305
+ def self.neatConvertWithUnitDisplay(double, fromString, toString, digits, unitBefore = false, unitAfter = true, space = true, parentheses = true)
306
+ # convert units
307
+ doubleConverted = OpenStudio.convert(double, fromString, toString)
308
+ if !doubleConverted.nil?
309
+ doubleConverted = doubleConverted.get
310
+ else
311
+ puts "Couldn't convert values, check string choices passed in. From: #{fromString}, To: #{toString}"
312
+ end
313
+
314
+ # get neat version of converted
315
+ neatConverted = OpenStudio.toNeatString(doubleConverted, digits, true)
316
+
317
+ # add prefix
318
+ if unitBefore
319
+ if space == true && parentheses == true
320
+ prefix = "(#{toString}) "
321
+ elsif space == true && parentheses == false
322
+ prefix = "(#{toString})"
323
+ elsif space == false && parentheses == true
324
+ prefix = "#{toString} "
325
+ else
326
+ prefix = toString.to_s
327
+ end
328
+ else
329
+ prefix = ''
330
+ end
331
+
332
+ # add suffix
333
+ if unitAfter
334
+ if space == true && parentheses == true
335
+ suffix = " (#{toString})"
336
+ elsif space == true && parentheses == false
337
+ suffix = "(#{toString})"
338
+ elsif space == false && parentheses == true
339
+ suffix = " #{toString}"
340
+ else
341
+ suffix = toString.to_s
342
+ end
343
+ else
344
+ suffix = ''
345
+ end
346
+
347
+ finalString = "#{prefix}#{neatConverted}#{suffix}"
348
+
349
+ return finalString
350
+ end
351
+
352
+ # helper that loops through lifecycle costs getting total costs under "Construction" and add to counter if occurs during year 0
353
+ def self.getTotalCostForObjects(objectArray, category = 'Construction', onlyYearFromStartZero = true)
354
+ counter = 0
355
+ objectArray.each do |object|
356
+ object_LCCs = object.lifeCycleCosts
357
+ object_LCCs.each do |object_LCC|
358
+ if object_LCC.category == category
359
+ if onlyYearFromStartZero == false || object_LCC.yearsFromStart == 0
360
+ counter += object_LCC.totalCost
361
+ end
362
+ end
363
+ end
364
+ end
365
+
366
+ return counter
367
+ end
368
+
369
+ # helper that loops through lifecycle costs getting total costs under "Construction" and add to counter if occurs during year 0
370
+ def self.getSpaceTypeStandardsInformation(spaceTypeArray)
371
+ # hash of space types
372
+ spaceTypeStandardsInfoHash = {}
373
+
374
+ spaceTypeArray.each do |spaceType|
375
+ # get standards building
376
+ if !spaceType.standardsBuildingType.empty?
377
+ standardsBuilding = spaceType.standardsBuildingType.get
378
+ else
379
+ standardsBuilding = nil
380
+ end
381
+
382
+ # get standards space type
383
+ if !spaceType.standardsSpaceType.empty?
384
+ standardsSpaceType = spaceType.standardsSpaceType.get
385
+ else
386
+ standardsSpaceType = nil
387
+ end
388
+
389
+ # populate hash
390
+ spaceTypeStandardsInfoHash[spaceType] = [standardsBuilding, standardsSpaceType]
391
+ end
392
+
393
+ return spaceTypeStandardsInfoHash
394
+ end
395
+
396
+ # OpenStudio has built in toNeatString method
397
+ # OpenStudio::toNeatString(double,2,true)# double,decimals, show commas
398
+
399
+ # OpenStudio has built in helper for unit conversion. That can be done using OpenStudio::convert() as shown below.
400
+ # OpenStudio::convert(double,"from unit string","to unit string").get
401
+ end