Line data Source code
1 : // EnergyPlus, Copyright (c) 1996-2025, The Board of Trustees of the University of Illinois,
2 : // The Regents of the University of California, through Lawrence Berkeley National Laboratory
3 : // (subject to receipt of any required approvals from the U.S. Dept. of Energy), Oak Ridge
4 : // National Laboratory, managed by UT-Battelle, Alliance for Sustainable Energy, LLC, and other
5 : // contributors. All rights reserved.
6 : //
7 : // NOTICE: This Software was developed under funding from the U.S. Department of Energy and the
8 : // U.S. Government consequently retains certain rights. As such, the U.S. Government has been
9 : // granted for itself and others acting on its behalf a paid-up, nonexclusive, irrevocable,
10 : // worldwide license in the Software to reproduce, distribute copies to the public, prepare
11 : // derivative works, and perform publicly and display publicly, and to permit others to do so.
12 : //
13 : // Redistribution and use in source and binary forms, with or without modification, are permitted
14 : // provided that the following conditions are met:
15 : //
16 : // (1) Redistributions of source code must retain the above copyright notice, this list of
17 : // conditions and the following disclaimer.
18 : //
19 : // (2) Redistributions in binary form must reproduce the above copyright notice, this list of
20 : // conditions and the following disclaimer in the documentation and/or other materials
21 : // provided with the distribution.
22 : //
23 : // (3) Neither the name of the University of California, Lawrence Berkeley National Laboratory,
24 : // the University of Illinois, U.S. Dept. of Energy nor the names of its contributors may be
25 : // used to endorse or promote products derived from this software without specific prior
26 : // written permission.
27 : //
28 : // (4) Use of EnergyPlus(TM) Name. If Licensee (i) distributes the software in stand-alone form
29 : // without changes from the version obtained under this License, or (ii) Licensee makes a
30 : // reference solely to the software portion of its product, Licensee must refer to the
31 : // software as "EnergyPlus version X" software, where "X" is the version number Licensee
32 : // obtained under this License and may not use a different name for the software. Except as
33 : // specifically required in this Section (4), Licensee shall not use in a company name, a
34 : // product name, in advertising, publicity, or other promotional activities any name, trade
35 : // name, trademark, logo, or other designation of "EnergyPlus", "E+", "e+" or confusingly
36 : // similar designation, without the U.S. Department of Energy's prior written consent.
37 : //
38 : // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
39 : // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
40 : // AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
41 : // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
42 : // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
43 : // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
44 : // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
45 : // OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
46 : // POSSIBILITY OF SUCH DAMAGE.
47 :
48 : // C++ Headers
49 : #include <thread>
50 :
51 : // CLI Headers
52 : #include <CLI/CLI11.hpp>
53 :
54 : // Project headers
55 : #include <EnergyPlus/CommandLineInterface.hh>
56 : #include <EnergyPlus/Data/EnergyPlusData.hh>
57 : #include <EnergyPlus/DataStringGlobals.hh>
58 : #include <EnergyPlus/DisplayRoutines.hh>
59 : #include <EnergyPlus/EnergyPlus.hh>
60 : #include <EnergyPlus/FileSystem.hh>
61 : #include <EnergyPlus/PluginManager.hh>
62 : #include <EnergyPlus/UtilityRoutines.hh>
63 :
64 : #if LINK_WITH_PYTHON
65 : # include <EnergyPlus/PythonEngine.hh>
66 : #endif
67 :
68 : namespace EnergyPlus {
69 :
70 : namespace CommandLineInterface {
71 :
72 55 : int ProcessArgs(EnergyPlusData &state, const std::vector<std::string> &args)
73 : {
74 : using size_type = std::string::size_type;
75 :
76 : // Expand long-name options using "=" sign in to two arguments
77 : // and expand multiple short options into separate arguments
78 55 : std::vector<std::string> arguments;
79 :
80 55 : std::string const dash("-");
81 :
82 318 : for (const auto &inputArg : args) {
83 :
84 263 : size_type const doubleDashPosition = inputArg.find("--");
85 263 : size_type const equalsPosition = inputArg.find("=");
86 :
87 263 : if (doubleDashPosition == 0 && equalsPosition != std::string::npos) { // --option=value
88 0 : arguments.push_back(inputArg.substr(0, equalsPosition));
89 0 : arguments.push_back(inputArg.substr(equalsPosition + 1, inputArg.size() - 1));
90 263 : } else if (doubleDashPosition == 0 && inputArg.size() == 2) {
91 : // Filter it out, it's a bash-like separator (end of the command options, before positionals arguments are accepted)
92 263 : } else if ((inputArg.size() > 2) && (inputArg[0] == '-') && (inputArg[1] != '-')) { // -abc style
93 0 : for (size_type c = 1; c < inputArg.size(); ++c) {
94 0 : arguments.push_back(dash + inputArg[c]);
95 : }
96 : } else { // ?
97 263 : arguments.push_back(inputArg);
98 : }
99 : }
100 :
101 : // erase the first element, which is the name of the program
102 55 : const std::string programName = std::move(arguments.front());
103 55 : arguments.erase(arguments.begin());
104 :
105 55 : size_type const argCount = arguments.size();
106 55 : bool const legacyMode = (argCount == 0);
107 :
108 : // Set path of EnergyPlus program path (if we aren't overriding it)
109 55 : if (!state.dataGlobal->installRootOverride) {
110 41 : state.dataStrGlobals->exeDirectoryPath = FileSystem::getParentDirectoryPath(FileSystem::getAbsolutePath(FileSystem::getProgramPath()));
111 : }
112 :
113 110 : CLI::App app{"energyplus", programName};
114 : // opt.add("", false, 0, 0, "Display version information", "-v", "--version");
115 165 : app.set_version_flag("-v,--version", EnergyPlus::DataStringGlobals::VerString);
116 :
117 : std::string const description = fmt::format(R"({}
118 : PythonLinkage: {}
119 : Built on Platform: {}
120 : )",
121 55 : state.dataStrGlobals->VerStringVar,
122 55 : PluginManagement::pythonStringForUsage(state),
123 55 : DataStringGlobals::BuildPlatformString);
124 55 : app.description(description);
125 :
126 220 : auto *annualOpt = app.add_flag("-a,--annual", state.dataGlobal->AnnualSimulation, "Force annual simulation");
127 :
128 220 : app.add_flag("-D,--design-day", state.dataGlobal->DDOnlySimulation, "Force design-day-only simulation")->excludes(annualOpt);
129 :
130 55 : app.add_option("-d,--output-directory", state.dataStrGlobals->outDirPath, "Output directory path (default: current directory)")
131 330 : ->option_text("DIR")
132 55 : ->required(false);
133 : // ->check(CLI::ExistingDirectory) // We don't require it to exist, we make it if needed
134 :
135 55 : if (legacyMode) {
136 2 : state.dataStrGlobals->inputIddFilePath = "Energy+.idd";
137 : } else {
138 53 : state.dataStrGlobals->inputIddFilePath = state.dataStrGlobals->exeDirectoryPath / "Energy+.idd";
139 : }
140 165 : app.add_option(
141 55 : "-i,--idd", state.dataStrGlobals->inputIddFilePath, "Input data dictionary path (default: Energy+.idd in executable directory)")
142 : ->required(false)
143 : ->option_text("IDD")
144 275 : ->check(CLI::ExistingFile);
145 :
146 55 : bool runEPMacro = false;
147 220 : app.add_flag("-m,--epmacro", runEPMacro, "Run EPMacro prior to simulation");
148 :
149 110 : std::string prefixOutName = "eplus";
150 330 : app.add_option("-p,--output-prefix", prefixOutName, "Prefix for output file names (default: eplus)")->required(false)->option_text("PRE");
151 :
152 220 : app.add_flag("-r,--readvars", state.dataGlobal->runReadVars, "Run ReadVarsESO after simulation");
153 :
154 220 : app.add_flag("-c,--convert", state.dataGlobal->outputEpJSONConversion, "Output IDF->epJSON or epJSON->IDF, dependent on input file type");
155 :
156 220 : app.add_flag("--convert-only",
157 55 : state.dataGlobal->outputEpJSONConversionOnly,
158 : "Only convert IDF->epJSON or epJSON->IDF, dependent on input file type. No simulation");
159 :
160 110 : std::string suffixType = "L";
161 55 : std::string const suffixHelp = R"help(Suffix style for output file names (default: L)
162 : L: Legacy (e.g., eplustbl.csv)
163 : C: Capital (e.g., eplusTable.csv)
164 : D: Dash (e.g., eplus-table.csv))help";
165 : app.add_option("-s,--output-suffix", suffixType, suffixHelp)
166 275 : ->option_text("SUFFIX")
167 : ->required(false)
168 165 : ->check(CLI::IsMember({"L", "C", "D"}, CLI::ignore_case));
169 :
170 : // TODO: maybe delay validation to output a better error message?
171 55 : const int MAX_N = static_cast<int>(std::thread::hardware_concurrency());
172 : app.add_option("-j,--jobs",
173 55 : state.dataGlobal->numThread,
174 : "Multi-thread with N threads; 1 thread with no arg. (Currently only for G-Function generation)")
175 : ->option_text("N")
176 : // ->check(CLI::Range(1, MAX_N) // Tempted to just do that... much simpler
177 : // ->check(CLI::Number)
178 495 : ->transform([MAX_N, &state](std::string input) -> std::string {
179 6 : int number_of_threads = -1;
180 6 : bool const converted = CLI::detail::lexical_cast(input, number_of_threads);
181 6 : if (!converted) {
182 : // CLI::ValidationError
183 0 : return fmt::format("Argument should be an integer, not '{}'", input);
184 : }
185 6 : if (number_of_threads <= 0) {
186 2 : DisplayString(state, "Invalid value for -j arg. Defaulting to 1.");
187 4 : return "1";
188 : }
189 4 : if (number_of_threads > MAX_N) {
190 2 : DisplayString(state,
191 4 : fmt::format("Invalid value for -j arg. Value exceeds num available. Defaulting to num available. -j {}", MAX_N));
192 2 : return std::to_string(MAX_N);
193 : }
194 2 : return input;
195 : });
196 :
197 55 : state.files.inputWeatherFilePath.filePath = "in.epw";
198 : // Note: we can't do check(CLI::ExistingFile) here since passing a non-existing /path/to/myfile.epw file
199 : // when there exists a /path/to/myfile.stat would mean the Weather File Statistics are still parsed and reported to the tabular output
200 : // We still report a message + fail later if DDOnlySimulation is false
201 : auto *weatherPathOpt =
202 165 : app.add_option("-w,--weather", state.files.inputWeatherFilePath.filePath, "Weather file path (default: in.epw in current directory)")
203 : ->required(false)
204 165 : ->option_text("EPW");
205 :
206 55 : bool runExpandObjects = false;
207 165 : app.add_flag("-x,--expandobjects", runExpandObjects, "Run ExpandObjects prior to simulation");
208 :
209 : // Positional
210 55 : state.dataStrGlobals->inputFilePath = "in.idf";
211 165 : app.add_option("input_file", state.dataStrGlobals->inputFilePath, "Input file (default: in.idf in current directory)")
212 : ->required(false)
213 165 : ->check(CLI::ExistingFile);
214 :
215 : // Catching it myself, so I can print the arguments vector before it's mutated
216 318 : bool debugCLI = std::any_of(args.begin(), args.end(), [](const auto &arg) { return arg == "--debug-cli"; });
217 55 : if (debugCLI) {
218 : {
219 0 : fmt::print("ProcessArgs: received args\n");
220 0 : int na = 0;
221 0 : for (const auto &a : args) {
222 0 : fmt::print("* {}: '{}'\n", na++, a);
223 : }
224 : }
225 : {
226 0 : fmt::print("\nAfter massaging/expanding of args\n");
227 0 : int na = 0;
228 0 : for (const auto &a : arguments) {
229 0 : fmt::print("* {}: '{}'\n", na++, a);
230 : }
231 : }
232 0 : fmt::print("\n");
233 : }
234 : // bool debugCLI = false;
235 330 : app.add_flag("--debug-cli", debugCLI, "Print the result of the CLI assignments to the console and exit")->group(""); // Empty group to hide it
236 :
237 : #if LINK_WITH_PYTHON
238 : # ifdef PYTHON_CLI
239 : auto *auxiliaryToolsSubcommand = app.add_subcommand("auxiliary", "Run Auxiliary Python Tools");
240 : auxiliaryToolsSubcommand->require_subcommand(); // should default to requiring 1 or more additional args?
241 :
242 : std::vector<std::string> python_fwd_args;
243 : auto *epLaunchSubCommand = auxiliaryToolsSubcommand->add_subcommand("eplaunch", "EnergyPlus Launch");
244 : epLaunchSubCommand->add_option("args", python_fwd_args, "Extra Arguments forwarded to EnergyPlus Launch")->option_text("ARG ...");
245 : epLaunchSubCommand->positionals_at_end(true);
246 : epLaunchSubCommand->footer("You can pass extra arguments after the eplaunch keyword, they will be forwarded to EnergyPlus Launch.");
247 :
248 : epLaunchSubCommand->callback([&state, &python_fwd_args] {
249 : EnergyPlus::Python::PythonEngine engine(state);
250 : // There's probably better to be done, like instantiating the pythonEngine with the argc/argv then calling PyRun_SimpleFile but whatever
251 : std::string cmd = Python::PythonEngine::getTclPreppedPreamble(python_fwd_args);
252 : cmd += R"python(
253 : from eplaunch.tk_runner import main_gui
254 : main_gui(True)
255 : )python";
256 : engine.exec(cmd);
257 : exit(0);
258 : });
259 :
260 : auto *updaterSubCommand = auxiliaryToolsSubcommand->add_subcommand("updater", "IDF Version Updater");
261 : updaterSubCommand->add_option("args", python_fwd_args, "Extra Arguments forwarded to IDF Version Updater")->option_text("ARG ...");
262 : updaterSubCommand->positionals_at_end(true);
263 : updaterSubCommand->footer("You can pass extra arguments after the updater keyword, they will be forwarded to IDF Version Updater.");
264 :
265 : updaterSubCommand->callback([&state, &python_fwd_args] {
266 : EnergyPlus::Python::PythonEngine engine(state);
267 : // There's probably better to be done, like instantiating the pythonEngine with the argc/argv then calling PyRun_SimpleFile but whatever
268 : std::string cmd = Python::PythonEngine::getTclPreppedPreamble(python_fwd_args);
269 : cmd += R"python(
270 : from energyplus_transition.runner import main_gui
271 : main_gui(True)
272 : )python";
273 : engine.exec(cmd);
274 : exit(0);
275 : });
276 : # endif
277 : #endif
278 :
279 55 : app.footer("Example: energyplus -w weather.epw -r input.idf");
280 :
281 55 : const bool eplusRunningViaAPI = state.dataGlobal->eplusRunningViaAPI;
282 :
283 : try {
284 : // app.parse(argc, argv);
285 : // CLI11 when passing argc, argv creates a vector<string> but **in reverse** order:
286 : // https://github.com/CLIUtils/CLI11/blob/291c58789c031208f08f4f261a858b5b7083e8e2/include/CLI/impl/App_inl.hpp#L476-L488
287 55 : std::reverse(arguments.begin(), arguments.end());
288 55 : app.parse(arguments);
289 6 : } catch (const CLI::Success &e) {
290 3 : int const return_code = app.exit(e);
291 3 : if (eplusRunningViaAPI) {
292 3 : return static_cast<int>(ReturnCodes::SuccessButHelper);
293 : } else {
294 0 : exit(return_code);
295 : }
296 6 : } catch (const CLI::ParseError &e) {
297 3 : int const return_code = app.exit(e);
298 3 : if (eplusRunningViaAPI) {
299 3 : return static_cast<int>(ReturnCodes::Failure);
300 : } else {
301 0 : exit(return_code);
302 : }
303 3 : }
304 :
305 49 : if (debugCLI) {
306 0 : fmt::print(stderr,
307 : R"debug(
308 : state.dataGlobal->AnnualSimulation = {},
309 : state.dataGlobal->DDOnlySimulation = {},
310 : state.dataStrGlobals->outDirPath = '{:g}',
311 : state.dataStrGlobals->inputIddFilePath= '{:g}',
312 :
313 : runEPMacro = {},
314 : prefixOutName = {},
315 :
316 : state.dataGlobal->runReadVars={},
317 : state.dataGlobal->outputEpJSONConversion={},
318 : state.dataGlobal->outputEpJSONConversionOnly={},
319 :
320 : suffixType={},
321 :
322 : state.dataGlobal->numThread={},
323 : state.files.inputWeatherFilePath.filePath='{:g}',
324 : state.dataStrGlobals->inputFilePath='{:g}',
325 : )debug",
326 0 : state.dataGlobal->AnnualSimulation,
327 0 : state.dataGlobal->DDOnlySimulation,
328 0 : state.dataStrGlobals->outDirPath,
329 0 : state.dataStrGlobals->inputIddFilePath,
330 :
331 : runEPMacro,
332 : prefixOutName,
333 0 : state.dataGlobal->runReadVars,
334 0 : state.dataGlobal->outputEpJSONConversion,
335 0 : state.dataGlobal->outputEpJSONConversionOnly,
336 : suffixType,
337 0 : state.dataGlobal->numThread,
338 0 : state.files.inputWeatherFilePath.filePath,
339 0 : state.dataStrGlobals->inputFilePath);
340 :
341 0 : fmt::print(stderr, "--debug-cli passed: exiting early\n");
342 :
343 0 : exit(0);
344 : }
345 :
346 : // Convert all paths to native paths
347 49 : state.dataStrGlobals->inputFilePath = FileSystem::makeNativePath(state.dataStrGlobals->inputFilePath);
348 49 : state.files.inputWeatherFilePath.filePath = FileSystem::makeNativePath(state.files.inputWeatherFilePath.filePath);
349 49 : state.dataStrGlobals->inputIddFilePath = FileSystem::makeNativePath(state.dataStrGlobals->inputIddFilePath);
350 49 : state.dataStrGlobals->outDirPath = FileSystem::makeNativePath(state.dataStrGlobals->outDirPath);
351 :
352 49 : state.dataStrGlobals->inputFilePathNameOnly = FileSystem::getFileName(state.dataStrGlobals->inputFilePath);
353 49 : state.dataStrGlobals->inputDirPath = FileSystem::getParentDirectoryPath(state.dataStrGlobals->inputFilePath);
354 :
355 : {
356 49 : FileSystem::FileTypes const fileType = FileSystem::getFileType(state.dataStrGlobals->inputFilePath);
357 49 : state.dataGlobal->isEpJSON = FileSystem::is_all_json_type(fileType);
358 49 : switch (fileType) {
359 49 : case FileSystem::FileTypes::IDF:
360 : case FileSystem::FileTypes::IMF:
361 : case FileSystem::FileTypes::EpJSON:
362 : case FileSystem::FileTypes::JSON:
363 49 : break;
364 0 : case FileSystem::FileTypes::CBOR:
365 0 : DisplayString(state, "CBOR input format is experimental and unsupported.");
366 0 : break;
367 0 : case FileSystem::FileTypes::MsgPack:
368 0 : DisplayString(state, "MsgPack input format is experimental and unsupported.");
369 0 : break;
370 0 : case FileSystem::FileTypes::UBJSON:
371 0 : DisplayString(state, "UBJSON input format is experimental and unsupported.");
372 0 : break;
373 0 : case FileSystem::FileTypes::BSON:
374 0 : DisplayString(state, "BSON input format is experimental and unsupported.");
375 0 : break;
376 0 : default:
377 0 : DisplayString(state,
378 0 : fmt::format("ERROR: Input file must have IDF, IMF, or epJSON extension: {:g}", state.dataStrGlobals->inputFilePath));
379 0 : if (eplusRunningViaAPI) {
380 0 : return static_cast<int>(ReturnCodes::Failure);
381 : } else {
382 0 : exit(EXIT_FAILURE);
383 : }
384 : }
385 : }
386 :
387 49 : if (!state.dataStrGlobals->outDirPath.empty()) {
388 : // Create directory if it doesn't already exist
389 25 : FileSystem::makeDirectory(state.dataStrGlobals->outDirPath);
390 : }
391 :
392 : // File naming scheme
393 98 : fs::path outputFilePrefixFullPath = state.dataStrGlobals->outDirPath / prefixOutName;
394 :
395 98 : fs::path outputEpmdetFilePath;
396 98 : fs::path outputEpmidfFilePath;
397 :
398 98 : fs::path outputExpidfFilePath;
399 98 : fs::path outputExperrFilePath;
400 :
401 98 : std::string normalSuffix;
402 98 : std::string tableSuffix;
403 98 : std::string mapSuffix;
404 98 : std::string zszSuffix;
405 98 : std::string spszSuffix;
406 98 : std::string sszSuffix;
407 98 : std::string meterSuffix;
408 98 : std::string sqliteSuffix;
409 98 : std::string adsSuffix;
410 98 : std::string screenSuffix;
411 98 : std::string shdSuffix;
412 :
413 98 : std::string const errorFollowUp = "Type 'energyplus --help' for usage.";
414 : {
415 49 : std::transform(suffixType.begin(), suffixType.end(), suffixType.begin(), ::toupper);
416 :
417 49 : if (suffixType == "L") {
418 :
419 47 : normalSuffix = "out";
420 47 : tableSuffix = "tbl";
421 47 : mapSuffix = "map";
422 47 : zszSuffix = "zsz";
423 47 : spszSuffix = "spsz";
424 47 : sszSuffix = "ssz";
425 47 : meterSuffix = "mtr";
426 47 : sqliteSuffix = "sqlite";
427 47 : adsSuffix = "ADS";
428 47 : screenSuffix = "screen";
429 47 : shdSuffix = "shading";
430 :
431 2 : } else if (suffixType == "D") {
432 :
433 1 : normalSuffix = "";
434 1 : tableSuffix = "-table";
435 1 : mapSuffix = "-map";
436 1 : zszSuffix = "-zsz";
437 1 : spszSuffix = "-spsz";
438 1 : sszSuffix = "-ssz";
439 1 : meterSuffix = "-meter";
440 1 : sqliteSuffix = "-sqlite";
441 1 : adsSuffix = "-ads";
442 1 : screenSuffix = "-screen";
443 1 : shdSuffix = "-shading";
444 :
445 1 : } else if (suffixType == "C") {
446 :
447 1 : normalSuffix = "";
448 1 : tableSuffix = "Table";
449 1 : mapSuffix = "Map";
450 1 : zszSuffix = "Zsz";
451 1 : spszSuffix = "Spsz";
452 1 : sszSuffix = "Ssz";
453 1 : meterSuffix = "Meter";
454 1 : sqliteSuffix = "Sqlite";
455 1 : adsSuffix = "Ads";
456 1 : screenSuffix = "Screen";
457 1 : shdSuffix = "Shading";
458 : }
459 : // No else needed, this is validated at CLI level above
460 : }
461 :
462 : // Helper to construct output file path
463 3726 : auto composePath = [&outputFilePrefixFullPath](const std::string &suffix) -> fs::path {
464 3726 : return FileSystem::appendSuffixToPath(outputFilePrefixFullPath, suffix);
465 49 : };
466 :
467 : // EnergyPlus files
468 49 : state.files.audit.filePath = composePath(normalSuffix + ".audit");
469 49 : state.files.bnd.filePath = composePath(normalSuffix + ".bnd");
470 49 : state.files.dxf.filePath = composePath(normalSuffix + ".dxf");
471 49 : state.files.eio.filePath = composePath(normalSuffix + ".eio");
472 49 : state.files.endFile.filePath = composePath(normalSuffix + ".end");
473 49 : state.files.outputErrFilePath = composePath(normalSuffix + ".err");
474 49 : state.files.eso.filePath = composePath(normalSuffix + ".eso");
475 :
476 49 : state.files.json.outputJsonFilePath = composePath(normalSuffix + ".json");
477 49 : state.files.json.outputTSZoneJsonFilePath = composePath(normalSuffix + "_detailed_zone.json");
478 49 : state.files.json.outputTSHvacJsonFilePath = composePath(normalSuffix + "_detailed_HVAC.json");
479 49 : state.files.json.outputTSJsonFilePath = composePath(normalSuffix + "_timestep.json");
480 49 : state.files.json.outputYRJsonFilePath = composePath(normalSuffix + "_yearly.json");
481 49 : state.files.json.outputMNJsonFilePath = composePath(normalSuffix + "_monthly.json");
482 49 : state.files.json.outputDYJsonFilePath = composePath(normalSuffix + "_daily.json");
483 49 : state.files.json.outputHRJsonFilePath = composePath(normalSuffix + "_hourly.json");
484 49 : state.files.json.outputSMJsonFilePath = composePath(normalSuffix + "_runperiod.json");
485 49 : state.files.json.outputCborFilePath = composePath(normalSuffix + ".cbor");
486 49 : state.files.json.outputTSZoneCborFilePath = composePath(normalSuffix + "_detailed_zone.cbor");
487 49 : state.files.json.outputTSHvacCborFilePath = composePath(normalSuffix + "_detailed_HVAC.cbor");
488 49 : state.files.json.outputTSCborFilePath = composePath(normalSuffix + "_timestep.cbor");
489 49 : state.files.json.outputYRCborFilePath = composePath(normalSuffix + "_yearly.cbor");
490 49 : state.files.json.outputMNCborFilePath = composePath(normalSuffix + "_monthly.cbor");
491 49 : state.files.json.outputDYCborFilePath = composePath(normalSuffix + "_daily.cbor");
492 49 : state.files.json.outputHRCborFilePath = composePath(normalSuffix + "_hourly.cbor");
493 49 : state.files.json.outputSMCborFilePath = composePath(normalSuffix + "_runperiod.cbor");
494 49 : state.files.json.outputMsgPackFilePath = composePath(normalSuffix + ".msgpack");
495 49 : state.files.json.outputTSZoneMsgPackFilePath = composePath(normalSuffix + "_detailed_zone.msgpack");
496 49 : state.files.json.outputTSHvacMsgPackFilePath = composePath(normalSuffix + "_detailed_HVAC.msgpack");
497 49 : state.files.json.outputTSMsgPackFilePath = composePath(normalSuffix + "_timestep.msgpack");
498 49 : state.files.json.outputYRMsgPackFilePath = composePath(normalSuffix + "_yearly.msgpack");
499 49 : state.files.json.outputMNMsgPackFilePath = composePath(normalSuffix + "_monthly.msgpack");
500 49 : state.files.json.outputDYMsgPackFilePath = composePath(normalSuffix + "_daily.msgpack");
501 49 : state.files.json.outputHRMsgPackFilePath = composePath(normalSuffix + "_hourly.msgpack");
502 49 : state.files.json.outputSMMsgPackFilePath = composePath(normalSuffix + "_runperiod.msgpack");
503 :
504 49 : state.files.mtd.filePath = composePath(normalSuffix + ".mtd");
505 49 : state.files.mdd.filePath = composePath(normalSuffix + ".mdd");
506 49 : state.files.mtr.filePath = composePath(normalSuffix + ".mtr");
507 49 : state.files.rdd.filePath = composePath(normalSuffix + ".rdd");
508 49 : state.dataStrGlobals->outputShdFilePath = composePath(normalSuffix + ".shd");
509 49 : state.files.dfs.filePath = composePath(normalSuffix + ".dfs");
510 49 : state.dataStrGlobals->outputGLHEFilePath = composePath(normalSuffix + ".glhe");
511 49 : state.files.edd.filePath = composePath(normalSuffix + ".edd");
512 49 : state.dataStrGlobals->outputIperrFilePath = composePath(normalSuffix + ".iperr");
513 49 : state.files.sln.filePath = composePath(normalSuffix + ".sln");
514 49 : state.files.sci.filePath = composePath(normalSuffix + ".sci");
515 49 : state.files.wrl.filePath = composePath(normalSuffix + ".wrl");
516 49 : state.dataStrGlobals->outputSqlFilePath = composePath(normalSuffix + ".sql");
517 49 : state.files.debug.filePath = composePath(normalSuffix + ".dbg");
518 49 : state.dataStrGlobals->outputPerfLogFilePath = composePath(normalSuffix + "_perflog.csv");
519 49 : state.dataStrGlobals->outputTblCsvFilePath = composePath(tableSuffix + ".csv");
520 49 : state.dataStrGlobals->outputTblHtmFilePath = composePath(tableSuffix + ".htm");
521 49 : state.dataStrGlobals->outputTblTabFilePath = composePath(tableSuffix + ".tab");
522 49 : state.dataStrGlobals->outputTblTxtFilePath = composePath(tableSuffix + ".txt");
523 49 : state.dataStrGlobals->outputTblXmlFilePath = composePath(tableSuffix + ".xml");
524 49 : state.files.outputMapTabFilePath = composePath(mapSuffix + ".tab");
525 49 : state.files.outputMapCsvFilePath = composePath(mapSuffix + ".csv");
526 49 : state.files.outputMapTxtFilePath = composePath(mapSuffix + ".txt");
527 49 : state.files.outputZszCsvFilePath = composePath(zszSuffix + ".csv");
528 49 : state.files.outputZszTabFilePath = composePath(zszSuffix + ".tab");
529 49 : state.files.outputZszTxtFilePath = composePath(zszSuffix + ".txt");
530 49 : state.files.outputSpszCsvFilePath = composePath(spszSuffix + ".csv");
531 49 : state.files.outputSpszTabFilePath = composePath(spszSuffix + ".tab");
532 49 : state.files.outputSpszTxtFilePath = composePath(spszSuffix + ".txt");
533 49 : state.files.outputSszCsvFilePath = composePath(sszSuffix + ".csv");
534 49 : state.files.outputSszTabFilePath = composePath(sszSuffix + ".tab");
535 49 : state.files.outputSszTxtFilePath = composePath(sszSuffix + ".txt");
536 49 : state.dataStrGlobals->outputAdsFilePath = composePath(adsSuffix + ".out");
537 49 : state.files.shade.filePath = composePath(shdSuffix + ".csv");
538 49 : if (suffixType == "L") {
539 : // out/sqlite.err
540 47 : state.dataStrGlobals->outputSqliteErrFilePath = state.dataStrGlobals->outDirPath / fs::path{sqliteSuffix + ".err"};
541 : } else {
542 : // if 'D': out/eplus-sqlite.err
543 2 : state.dataStrGlobals->outputSqliteErrFilePath = composePath(sqliteSuffix + ".err");
544 : }
545 :
546 49 : state.files.screenCsv.filePath = composePath(screenSuffix + ".csv");
547 : // TODO, why are these relative paths?
548 49 : state.files.delightIn.filePath = "eplusout.delightin";
549 49 : state.dataStrGlobals->outputDelightOutFilePath = "eplusout.delightout";
550 :
551 : // TODO: why is this relative?
552 49 : state.files.iniFile.filePath = "Energy+.ini";
553 :
554 : // Stat next to epw
555 : // Careful! fs::path::replace_extension will **mutate the original object**
556 49 : state.files.inStatFilePath.filePath = state.files.inputWeatherFilePath.filePath;
557 49 : state.files.inStatFilePath.filePath.replace_extension(".stat");
558 : // Or is it better to provide a helper that does not mutate like this?
559 : // state.files.inStatFilePath.filePath = FileSystem::replaceFileExtension(state.inputWeatherFilePath.filePath, ".stat");
560 :
561 49 : state.dataStrGlobals->eplusADSFilePath = state.dataStrGlobals->inputDirPath / "eplusADS.inp";
562 :
563 : // Readvars files
564 49 : state.files.csv.filePath = composePath(normalSuffix + ".csv");
565 49 : state.files.mtr_csv.filePath = composePath(meterSuffix + ".csv");
566 49 : state.dataStrGlobals->outputRvauditFilePath = composePath(normalSuffix + ".rvaudit");
567 :
568 : // EPMacro files
569 49 : outputEpmdetFilePath = composePath(normalSuffix + ".epmdet");
570 49 : outputEpmidfFilePath = composePath(normalSuffix + ".epmidf");
571 :
572 : // ExpandObjects files
573 49 : outputExpidfFilePath = composePath(normalSuffix + ".expidf");
574 49 : outputExperrFilePath = composePath(normalSuffix + ".experr");
575 :
576 : // Read path from INI file if it exists
577 :
578 : // Check for IDD and IDF files
579 49 : if (FileSystem::fileExists(state.files.iniFile.filePath)) {
580 0 : EnergyPlus::InputFile iniFile = state.files.iniFile.try_open();
581 0 : if (!iniFile.good()) {
582 0 : DisplayString(state, fmt::format("ERROR: Could not open file {} for input (read).", iniFile.filePath));
583 0 : if (eplusRunningViaAPI) {
584 0 : return static_cast<int>(ReturnCodes::Failure);
585 : } else {
586 0 : exit(EXIT_FAILURE);
587 : }
588 : }
589 0 : state.dataStrGlobals->CurrentWorkingFolder = iniFile.filePath;
590 : // Relying on compiler to supply full path name here
591 : // TODO: not sure I understand this block
592 : // const int TempIndx = index(state.dataStrGlobals->CurrentWorkingFolder, state.dataStrGlobals->pathChar, true);
593 : // if (TempIndx == std::string::npos) {
594 : // state.dataStrGlobals->CurrentWorkingFolder = "";
595 : //} else {
596 : // state.dataStrGlobals->CurrentWorkingFolder.erase(TempIndx + 1);
597 : //}
598 : // Get directories from ini file
599 0 : std::string programPathStr;
600 0 : ReadINIFile(iniFile, "program", "dir", programPathStr);
601 0 : state.dataStrGlobals->ProgramPath = fs::path(programPathStr);
602 :
603 0 : state.dataStrGlobals->inputIddFilePath = state.dataStrGlobals->ProgramPath / "Energy+.idd";
604 0 : }
605 :
606 : // Check if specified files exist
607 49 : if (!FileSystem::fileExists(state.dataStrGlobals->inputFilePath)) {
608 0 : DisplayString(
609 0 : state, fmt::format("ERROR: Could not find input data file: {}.", FileSystem::getAbsolutePath(state.dataStrGlobals->inputFilePath)));
610 0 : DisplayString(state, errorFollowUp);
611 0 : if (eplusRunningViaAPI) {
612 0 : return static_cast<int>(ReturnCodes::Failure);
613 : } else {
614 0 : exit(EXIT_FAILURE);
615 : }
616 : }
617 :
618 49 : if ((weatherPathOpt->count() > 0) && !state.dataGlobal->DDOnlySimulation) {
619 3 : if (!FileSystem::fileExists(state.files.inputWeatherFilePath.filePath)) {
620 2 : DisplayString(
621 : state,
622 4 : fmt::format("ERROR: Could not find weather file: {}.", FileSystem::getAbsolutePath(state.files.inputWeatherFilePath.filePath)));
623 2 : DisplayString(state, errorFollowUp);
624 2 : if (eplusRunningViaAPI) {
625 2 : return static_cast<int>(ReturnCodes::Failure);
626 : } else {
627 0 : exit(EXIT_FAILURE);
628 : }
629 : }
630 : }
631 :
632 : // TODO: might be able to convert epJSON->IDF, run preprocessors, then go back IDF->epJSON
633 :
634 : // Preprocessors (These will likely move to a new file)
635 47 : if (runEPMacro) {
636 0 : fs::path epMacroPath = (state.dataStrGlobals->exeDirectoryPath / "EPMacro").replace_extension(FileSystem::exeExtension);
637 0 : if (!FileSystem::fileExists(epMacroPath)) {
638 0 : DisplayString(state, fmt::format("ERROR: Could not find EPMacro executable: {}.", FileSystem::getAbsolutePath(epMacroPath)));
639 0 : if (eplusRunningViaAPI) {
640 0 : return static_cast<int>(ReturnCodes::Failure);
641 : } else {
642 0 : exit(EXIT_FAILURE);
643 : }
644 : }
645 0 : std::string epMacroCommand = "\"" + FileSystem::toString(epMacroPath) + "\"";
646 0 : bool inputFilePathdIn = (FileSystem::getAbsolutePath(state.dataStrGlobals->inputFilePath) == FileSystem::getAbsolutePath("in.imf"));
647 :
648 0 : if (!inputFilePathdIn) {
649 0 : FileSystem::linkFile(state.dataStrGlobals->inputFilePath, "in.imf");
650 : }
651 0 : DisplayString(state, "Running EPMacro...");
652 0 : FileSystem::systemCall(epMacroCommand);
653 0 : if (!inputFilePathdIn) {
654 0 : FileSystem::removeFile("in.imf");
655 : }
656 0 : FileSystem::moveFile("audit.out", outputEpmdetFilePath);
657 0 : FileSystem::moveFile("out.idf", outputEpmidfFilePath);
658 0 : state.dataStrGlobals->inputFilePath = outputEpmidfFilePath;
659 0 : }
660 :
661 47 : if (runExpandObjects) {
662 : fs::path expandObjectsPath =
663 0 : (state.dataStrGlobals->exeDirectoryPath / fs::path("ExpandObjects")).replace_extension(FileSystem::exeExtension);
664 0 : if (!FileSystem::fileExists(expandObjectsPath)) {
665 0 : DisplayString(state,
666 0 : fmt::format("ERROR: Could not find ExpandObjects executable: {}.", FileSystem::getAbsolutePath(expandObjectsPath)));
667 0 : if (eplusRunningViaAPI) {
668 0 : return static_cast<int>(ReturnCodes::Failure);
669 : } else {
670 0 : exit(EXIT_FAILURE);
671 : }
672 : }
673 0 : std::string expandObjectsCommand = "\"" + FileSystem::toString(expandObjectsPath) + "\"";
674 0 : bool inputFilePathdIn = (FileSystem::getAbsolutePath(state.dataStrGlobals->inputFilePath) == FileSystem::getAbsolutePath("in.idf"));
675 :
676 : // check if IDD actually exists since ExpandObjects still requires it
677 0 : if (!FileSystem::fileExists(state.dataStrGlobals->inputIddFilePath)) {
678 0 : DisplayString(state,
679 0 : fmt::format("ERROR: Could not find input data dictionary: {}.",
680 0 : FileSystem::getAbsolutePath(state.dataStrGlobals->inputIddFilePath)));
681 0 : DisplayString(state, errorFollowUp);
682 0 : if (eplusRunningViaAPI) {
683 0 : return static_cast<int>(ReturnCodes::Failure);
684 : } else {
685 0 : exit(EXIT_FAILURE);
686 : }
687 : }
688 :
689 : bool iddFilePathdEnergy =
690 0 : (FileSystem::getAbsolutePath(state.dataStrGlobals->inputIddFilePath) == FileSystem::getAbsolutePath("Energy+.idd"));
691 :
692 0 : if (!inputFilePathdIn) {
693 0 : FileSystem::linkFile(state.dataStrGlobals->inputFilePath, "in.idf");
694 : }
695 0 : if (!iddFilePathdEnergy) {
696 0 : FileSystem::linkFile(state.dataStrGlobals->inputIddFilePath, "Energy+.idd");
697 : }
698 :
699 0 : FileSystem::systemCall(expandObjectsCommand);
700 0 : if (!inputFilePathdIn) {
701 0 : FileSystem::removeFile("in.idf");
702 : }
703 0 : if (!iddFilePathdEnergy) {
704 0 : FileSystem::removeFile("Energy+.idd");
705 : }
706 :
707 0 : FileSystem::moveFile("expandedidf.err", outputExperrFilePath);
708 0 : if (FileSystem::fileExists("expanded.idf")) {
709 0 : FileSystem::moveFile("expanded.idf", outputExpidfFilePath);
710 0 : state.dataStrGlobals->inputFilePath = outputExpidfFilePath;
711 : }
712 0 : }
713 :
714 47 : return static_cast<int>(ReturnCodes::Success);
715 55 : }
716 :
717 : // Fix This is Fortranic code that needs to be brought up to C++ style
718 : // All the index and len and strip should be eliminated and replaced by string calls only where needed
719 : // I/o with std::string should not be pulling in trailing blanks so stripping should not be needed, etc.
720 : // Rewinding is a big performance hit and should be avoided if possible
721 : // Case-insensitive comparison is much faster than converting strings to upper or lower case
722 : // Each strip and case conversion is a heap hit and should be avoided if possible
723 0 : void ReadINIFile(InputFile &inputFile, // Unit number of the opened INI file
724 : std::string const &Heading, // Heading for the parameters ('[heading]')
725 : std::string const &KindofParameter, // Kind of parameter to be found (String)
726 : std::string &DataOut // Output from the retrieval
727 : )
728 : {
729 :
730 : // SUBROUTINE INFORMATION:
731 : // AUTHOR Linda K. Lawrie
732 : // DATE WRITTEN September 1997
733 : // MODIFIED na
734 : // RE-ENGINEERED na
735 :
736 : // PURPOSE OF THIS SUBROUTINE:
737 : // This routine reads the .ini file and retrieves
738 : // the path names for the files from it.
739 :
740 : // METHODOLOGY EMPLOYED:
741 : // Duplicate the kind of reading the Windows "GetINISetting" would
742 : // do.
743 :
744 : // Using/Aliasing
745 : using namespace EnergyPlus;
746 : using namespace DataStringGlobals;
747 :
748 0 : std::string Param;
749 : std::string::size_type ILB;
750 : std::string::size_type IRB;
751 : std::string::size_type IEQ;
752 : std::string::size_type IPAR;
753 : std::string::size_type IPOS;
754 :
755 : // Formats
756 :
757 0 : DataOut.clear();
758 :
759 : // I tried ADJUSTL(TRIM(KindofParameter)) and got an internal compiler error
760 :
761 0 : Param = KindofParameter;
762 0 : strip(Param);
763 0 : inputFile.rewind();
764 0 : bool Found = false;
765 0 : bool NewHeading = false;
766 :
767 0 : while (inputFile.good() && !Found) {
768 0 : EnergyPlus::InputFile::ReadResult const readResult = inputFile.readLine();
769 :
770 0 : if (readResult.eof) {
771 0 : break;
772 : }
773 :
774 0 : if (readResult.data.empty()) {
775 0 : continue;
776 : } // Ignore Blank Lines
777 :
778 0 : std::string LINEOut;
779 0 : ConvertCaseToLower(readResult.data, LINEOut); // Turn line into lower case
780 : // LINE=LINEOut
781 :
782 0 : if (!has(LINEOut, Heading)) continue;
783 :
784 : // See if [ and ] are on line
785 0 : ILB = index(LINEOut, '[');
786 0 : IRB = index(LINEOut, ']');
787 0 : if (ILB == std::string::npos && IRB == std::string::npos) continue;
788 0 : if (!has(LINEOut, '[' + Heading + ']')) continue; // Must be really correct heading line
789 :
790 : // Heading line found, now looking for Kind
791 0 : while (inputFile.good() && !NewHeading) {
792 0 : const auto innerReadResult = inputFile.readLine(); // readLine returns a ReadResult<std::string>, hence no & (THIS_AUTO_OK)
793 0 : if (innerReadResult.eof) {
794 0 : break;
795 : }
796 :
797 0 : std::string line = innerReadResult.data;
798 0 : strip(line);
799 :
800 0 : if (line.empty()) continue; // Ignore Blank Lines
801 :
802 0 : ConvertCaseToLower(line, LINEOut); // Turn line into lower case
803 : // LINE=LINEOut
804 :
805 0 : ILB = index(LINEOut, '[');
806 0 : IRB = index(LINEOut, ']');
807 0 : NewHeading = (ILB != std::string::npos && IRB != std::string::npos);
808 :
809 : // Should be a parameter line
810 : // KindofParameter = string
811 0 : IEQ = index(LINEOut, '=');
812 0 : IPAR = index(LINEOut, Param);
813 0 : if (IEQ == std::string::npos) continue;
814 0 : if (IPAR == std::string::npos) continue;
815 0 : if (IPAR != 0) continue;
816 0 : if (!has(LINEOut, Param + '=')) continue; // needs to be param=
817 :
818 : // = found and parameter found.
819 0 : if (IPAR > IEQ) continue;
820 :
821 : // parameter = found
822 : // Set output string to start with non-blank character
823 :
824 0 : DataOut = stripped(line.substr(IEQ + 1));
825 0 : Found = true;
826 0 : break;
827 0 : }
828 0 : }
829 :
830 0 : if (Param == "dir") {
831 0 : IPOS = len(DataOut);
832 0 : if (IPOS != 0) {
833 : // Non-blank make sure last position is valid path character
834 : // (Set in DataStringGlobals)
835 :
836 0 : if (DataOut[IPOS - 1] != pathChar) {
837 0 : DataOut += pathChar;
838 : }
839 : }
840 : }
841 0 : }
842 :
843 3 : int runReadVarsESO(EnergyPlusData &state)
844 : {
845 3 : fs::path readVarsPath = (state.dataStrGlobals->exeDirectoryPath / "ReadVarsESO").replace_extension(FileSystem::exeExtension);
846 :
847 3 : if (!FileSystem::fileExists(readVarsPath)) {
848 0 : readVarsPath = (state.dataStrGlobals->exeDirectoryPath / "PostProcess" / "ReadVarsESO").replace_extension(FileSystem::exeExtension);
849 0 : if (!FileSystem::fileExists(readVarsPath)) {
850 : // should report the error differently if the user is calling into E+ through EXE or DLL
851 0 : if (state.dataGlobal->eplusRunningViaAPI) {
852 0 : DisplayString(
853 : state,
854 : "ERROR: Could not find ReadVarsESO executable. When calling through C API, make sure to call setEnergyPlusRootDirectory");
855 : } else {
856 0 : DisplayString(state, fmt::format("ERROR: Could not find ReadVarsESO executable: {}.", FileSystem::getAbsolutePath(readVarsPath)));
857 : }
858 0 : return static_cast<int>(ReturnCodes::Failure);
859 : }
860 : }
861 :
862 3 : fs::path const RVIfile = (state.dataStrGlobals->inputDirPath / state.dataStrGlobals->inputFilePathNameOnly).replace_extension(".rvi");
863 3 : fs::path const MVIfile = (state.dataStrGlobals->inputDirPath / state.dataStrGlobals->inputFilePathNameOnly).replace_extension(".mvi");
864 :
865 3 : const bool rviFileExists = FileSystem::fileExists(RVIfile);
866 3 : if (!rviFileExists) {
867 3 : std::ofstream ofs{RVIfile};
868 3 : if (!ofs.good()) {
869 0 : ShowFatalError(state, format("EnergyPlus: Could not open file \"{}\" for output (write).", RVIfile));
870 : } else {
871 3 : ofs << FileSystem::toString(state.files.eso.filePath) << '\n';
872 3 : ofs << FileSystem::toString(state.files.csv.filePath) << '\n';
873 : }
874 3 : }
875 :
876 3 : const bool mviFileExists = FileSystem::fileExists(MVIfile);
877 3 : if (!mviFileExists) {
878 3 : std::ofstream ofs{MVIfile};
879 3 : if (!ofs.good()) {
880 0 : ShowFatalError(state, format("EnergyPlus: Could not open file \"{}\" for output (write).", RVIfile));
881 : } else {
882 3 : ofs << FileSystem::toString(state.files.mtr.filePath) << '\n';
883 3 : ofs << FileSystem::toString(state.files.mtr_csv.filePath) << '\n';
884 : }
885 3 : }
886 :
887 : // We quote the paths in case we have spaces
888 : // "/Path/to/ReadVarEso" "/Path/to/folder with spaces/file.rvi" unlimited
889 3 : std::string const readVarsRviCommand = "\"" + FileSystem::toString(readVarsPath) + "\" \"" + FileSystem::toString(RVIfile) + "\" unlimited";
890 3 : std::string const readVarsMviCommand = "\"" + FileSystem::toString(readVarsPath) + "\" \"" + FileSystem::toString(MVIfile) + "\" unlimited";
891 :
892 : // systemCall will be responsible to handle to above command on Windows versus Unix
893 3 : FileSystem::systemCall(readVarsRviCommand);
894 3 : FileSystem::systemCall(readVarsMviCommand);
895 :
896 3 : if (!rviFileExists) {
897 3 : FileSystem::removeFile(RVIfile);
898 : }
899 :
900 3 : if (!mviFileExists) {
901 3 : FileSystem::removeFile(MVIfile);
902 : }
903 :
904 3 : FileSystem::moveFile("readvars.audit", state.dataStrGlobals->outputRvauditFilePath);
905 3 : return static_cast<int>(ReturnCodes::Success);
906 3 : }
907 :
908 : } // namespace CommandLineInterface
909 : } // namespace EnergyPlus
|