Line data Source code
1 : //* This file is part of the MOOSE framework
2 : //* https://mooseframework.inl.gov
3 : //*
4 : //* All rights reserved, see COPYRIGHT for full restrictions
5 : //* https://github.com/idaholab/moose/blob/master/COPYRIGHT
6 : //*
7 : //* Licensed under LGPL 2.1, please see LICENSE for details
8 : //* https://www.gnu.org/licenses/lgpl-2.1.html
9 :
10 : #include "Exodus.h"
11 :
12 : // Moose includes
13 : #include "DisplacedProblem.h"
14 : #include "ExodusFormatter.h"
15 : #include "FEProblem.h"
16 : #include "FileMesh.h"
17 : #include "MooseApp.h"
18 : #include "MooseVariableScalar.h"
19 : #include "LockFile.h"
20 :
21 : #include "libmesh/exodusII_io.h"
22 : #include "libmesh/libmesh_config.h" // LIBMESH_HAVE_HDF5
23 :
24 : using namespace libMesh;
25 :
26 : registerMooseObject("MooseApp", Exodus);
27 :
28 : InputParameters
29 78786 : Exodus::validParams()
30 : {
31 : // Get the base class parameters
32 78786 : InputParameters params = SampledOutput::validParams();
33 : params +=
34 157572 : AdvancedOutput::enableOutputTypes("nodal elemental scalar postprocessor reporter input");
35 :
36 : // Enable sequential file output (do not set default, the use_displace criteria relies on
37 : // isParamValid, see Constructor)
38 315144 : params.addParam<bool>("sequence",
39 : "Enable/disable sequential file output (enabled by default "
40 : "when 'use_displace = true', otherwise defaults to false");
41 :
42 : // Select problem dimension for mesh output
43 472716 : params.addDeprecatedParam<bool>("use_problem_dimension",
44 : "Use the problem dimension to the mesh output. "
45 : "Set to false when outputting lower dimensional "
46 : "meshes embedded in a higher dimensional space.",
47 : "Use 'output_dimension = problem_dimension' instead.");
48 :
49 315144 : MooseEnum output_dimension("default 1 2 3 problem_dimension", "default");
50 :
51 315144 : params.addParam<MooseEnum>(
52 : "output_dimension", output_dimension, "The dimension of the output file");
53 :
54 315144 : params.addParamNamesToGroup("output_dimension", "Advanced");
55 :
56 : // Set the default padding to 3
57 157572 : params.set<unsigned int>("padding") = 3;
58 :
59 : // Add description for the Exodus class
60 157572 : params.addClassDescription("Object for output data in the Exodus format");
61 :
62 : // Flag for overwriting at each timestep
63 236358 : params.addParam<bool>("overwrite",
64 157572 : false,
65 : "When true the latest timestep will overwrite the "
66 : "existing file, so only a single timestep exists.");
67 :
68 : // Set outputting of the input to be on by default
69 157572 : params.set<ExecFlagEnum>("execute_input_on") = EXEC_INITIAL;
70 :
71 : // Flag for outputting discontinuous data to Exodus
72 236358 : params.addParam<bool>(
73 157572 : "discontinuous", false, "Enables discontinuous output format for Exodus files.");
74 :
75 : // Flag for outputting added side elements (for side-discontinuous data) to Exodus
76 236358 : params.addParam<bool>(
77 157572 : "side_discontinuous", false, "Enables adding side-discontinuous output in Exodus files.");
78 :
79 : // Flag for outputting Exodus data in HDF5 format (when libMesh is
80 : // configured with HDF5 support). libMesh wants to do so by default
81 : // (for backwards compatibility with libMesh HDF5 users), but we
82 : // want to avoid this by default (for backwards compatibility with
83 : // most Moose users and to avoid generating regression test gold
84 : // files that non-HDF5 Moose builds can't read)
85 315144 : params.addParam<bool>("write_hdf5", false, "Enables HDF5 output format for Exodus files.");
86 :
87 : // Set output of names to be truncated to a certain character count.
88 : // libMesh+ExodusII currently supports up to 80, so we would like to
89 : // default to that to avoid truncation when possible.
90 : //
91 : // We used to truncate at 32, so we make this user-configurable to
92 : // make it easier to match old gold files.
93 : //
94 : // We're still defaulting to 32 until our apps in CI start using
95 : // this option (and/or re-golding) downstream.
96 : //
97 : // If someone tries to set truncation at less than 32 they're
98 : // probably making a mistake.
99 393930 : params.addRangeCheckedParam<unsigned int>("max_output_name_length",
100 157572 : 32,
101 : "32<=max_output_name_length<=80",
102 : "Maximum length for names in Exodus file output.");
103 :
104 236358 : params.addParamNamesToGroup("write_hdf5 max_output_name_length", "Advanced");
105 :
106 : // Need a layer of geometric ghosting for mesh serialization
107 236358 : params.addRelationshipManager("ElementPointNeighborLayers",
108 : Moose::RelationshipManagerType::GEOMETRIC);
109 :
110 : // Return the InputParameters
111 157572 : return params;
112 78786 : }
113 :
114 36801 : Exodus::Exodus(const InputParameters & parameters)
115 : : SampledOutput(parameters),
116 36792 : _exodus_initialized(false),
117 73584 : _exodus_mesh_changed(declareRestartableData<bool>("exodus_mesh_changed", true)),
118 110400 : _sequence(isParamValid("sequence") ? getParam<bool>("sequence")
119 36768 : : _use_displaced ? true
120 : : false),
121 73584 : _exodus_num(declareRestartableData<unsigned int>("exodus_num", 0)),
122 36792 : _recovering(_app.isRecovering()),
123 73584 : _overwrite(getParam<bool>("overwrite")),
124 73584 : _output_dimension(getParam<MooseEnum>("output_dimension").getEnum<OutputDimension>()),
125 73584 : _discontinuous(getParam<bool>("discontinuous")),
126 73584 : _side_discontinuous(getParam<bool>("side_discontinuous")),
127 73584 : _write_hdf5(getParam<bool>("write_hdf5")),
128 147177 : _max_output_name_length(getParam<unsigned int>("max_output_name_length"))
129 : {
130 110376 : if (isParamValid("use_problem_dimension"))
131 : {
132 0 : auto use_problem_dimension = getParam<bool>("use_problem_dimension");
133 :
134 0 : if (use_problem_dimension)
135 0 : _output_dimension = OutputDimension::PROBLEM_DIMENSION;
136 : else
137 0 : _output_dimension = OutputDimension::DEFAULT;
138 : }
139 : // If user sets 'discontinuous = true' and 'elemental_as_nodal = false', issue an error that these
140 : // are incompatible states
141 36864 : if (_discontinuous && parameters.isParamSetByUser("elemental_as_nodal") && !_elemental_as_nodal)
142 0 : mooseError(name(),
143 : ": Invalid parameters. 'elemental_as_nodal' set to false while 'discontinuous' set "
144 : "to true.");
145 : // At this point, if we have discontinuous ouput, we know the user hasn't explicitly set
146 : // 'elemental_as_nodal = false', so we can safely default it to true
147 36792 : if (_discontinuous)
148 36 : _elemental_as_nodal = true;
149 36792 : }
150 :
151 : void
152 0 : Exodus::setOutputDimension(unsigned int /*dim*/)
153 : {
154 0 : mooseDeprecated(
155 : "This method is no longer needed. We can determine output dimension programmatically");
156 0 : }
157 :
158 : void
159 36444 : Exodus::initialSetup()
160 : {
161 : // Call base class setup method
162 36444 : SampledOutput::initialSetup();
163 :
164 : // The libMesh::ExodusII_IO will fail when it is closed if the object is created but
165 : // nothing is written to the file. This checks that at least something will be written.
166 36441 : if (!hasOutput())
167 0 : mooseError("The current settings result in nothing being output to the Exodus file.");
168 :
169 : // Test that some sort of variable output exists (case when all variables are disabled but input
170 : // output is still enabled
171 36486 : if (!hasNodalVariableOutput() && !hasElementalVariableOutput() && !hasPostprocessorOutput() &&
172 45 : !hasScalarOutput())
173 3 : mooseError("The current settings results in only the input file and no variables being output "
174 : "to the Exodus file, this is not supported.");
175 36438 : }
176 :
177 : void
178 5273 : Exodus::meshChanged()
179 : {
180 : // Maintain Sampled::meshChanged() functionality
181 5273 : SampledOutput::meshChanged();
182 :
183 : // Indicate to the Exodus object that the mesh has changed
184 5273 : _exodus_mesh_changed = true;
185 5273 : }
186 :
187 : void
188 0 : Exodus::sequence(bool state)
189 : {
190 0 : _sequence = state;
191 0 : }
192 :
193 : void
194 163718 : Exodus::outputSetup()
195 : {
196 163718 : if (_exodus_io_ptr)
197 : {
198 : // Do nothing if the ExodusII_IO objects exists, but has not been initialized
199 129041 : if (!_exodus_initialized)
200 125457 : return;
201 :
202 : // Do nothing if the output is using oversampling. In this case the mesh that is being output
203 : // has not been changed, so there is no need to create a new ExodusII_IO object
204 123253 : if (_use_sampled_output)
205 293 : return;
206 :
207 : // Do nothing if the mesh has not changed and sequential output is not desired
208 122960 : if (!_exodus_mesh_changed && !_sequence)
209 119376 : return;
210 : }
211 :
212 40349 : auto serialize = [this](auto & moose_mesh)
213 : {
214 40349 : auto & lm_mesh = moose_mesh.getMesh();
215 : // Exodus is serial output so that we have to gather everything to "zero".
216 40349 : lm_mesh.gather_to_zero();
217 : // This makes the face information out-of-date on process 0 for distributed meshes, e.g.
218 : // elements will have neighbors that they didn't previously have
219 40349 : if ((this->processor_id() == 0) && !lm_mesh.is_replicated())
220 4132 : moose_mesh.markFiniteVolumeInfoDirty();
221 78610 : };
222 38261 : serialize(_problem_ptr->mesh());
223 :
224 : // We need to do the same thing for displaced mesh to make them consistent.
225 : // In general, it is a good idea to make the reference mesh and the displaced mesh
226 : // consistent since some operations or calculations are already based on this assumption.
227 : // For example,
228 : // FlagElementsThread::onElement(const Elem * elem)
229 : // if (_displaced_problem)
230 : // _displaced_problem->mesh().elemPtr(elem->id())->set_refinement_flag((Elem::RefinementState)marker_value);
231 : // Here we assume that the displaced mesh and the reference mesh are identical except
232 : // coordinations.
233 38261 : if (_problem_ptr->getDisplacedProblem())
234 2088 : serialize(_problem_ptr->getDisplacedProblem()->mesh());
235 :
236 : // Create the ExodusII_IO object
237 38261 : _exodus_io_ptr = std::make_unique<ExodusII_IO>(_es_ptr->get_mesh());
238 38261 : _exodus_initialized = false;
239 :
240 38261 : if (_write_hdf5)
241 : {
242 : #ifndef LIBMESH_HAVE_HDF5
243 : mooseError("Moose input requested HDF Exodus output, but libMesh was built without HDF5.");
244 : #endif
245 :
246 : // This is redundant unless the libMesh default changes
247 0 : _exodus_io_ptr->set_hdf5_writing(true);
248 : }
249 : else
250 : {
251 38261 : _exodus_io_ptr->set_hdf5_writing(false);
252 : }
253 :
254 38261 : _exodus_io_ptr->set_max_name_length(_max_output_name_length);
255 :
256 38261 : if (_side_discontinuous)
257 22 : _exodus_io_ptr->write_added_sides(true);
258 :
259 : // Increment file number and set appending status, append if all the following conditions are met:
260 : // (1) If the application is recovering (not restarting)
261 : // (2) The mesh has NOT changed
262 : // (3) An existing Exodus file exists for appending (_exodus_num > 0)
263 : // (4) Sequential output is NOT desired
264 : // (5) Exodus is NOT being output only on FINAL
265 39080 : if (_recovering && !_exodus_mesh_changed && _exodus_num > 0 && !_sequence &&
266 819 : (getExecuteOnEnum().size() != 1 || !getExecuteOnEnum().contains(EXEC_FINAL)))
267 : {
268 : // Set the recovering flag to false so that this special case is not triggered again
269 817 : _recovering = false;
270 :
271 : // Set the append flag to true b/c on recover the file is being appended
272 817 : _exodus_io_ptr->append(true);
273 : }
274 : else
275 : {
276 : // Disable file appending and reset exodus file number count
277 37444 : _exodus_io_ptr->append(false);
278 :
279 : // Customize file output
280 37444 : customizeFileOutput();
281 : }
282 :
283 38261 : setOutputDimensionInExodusWriter(*_exodus_io_ptr, *_mesh_ptr, _output_dimension);
284 : }
285 :
286 : void
287 37444 : Exodus::customizeFileOutput()
288 : {
289 37444 : if (_exodus_mesh_changed || _sequence)
290 36630 : _file_num++;
291 :
292 37444 : _exodus_num = 1;
293 37444 : }
294 :
295 : void
296 41981 : Exodus::setOutputDimensionInExodusWriter(ExodusII_IO & exodus_io,
297 : const MooseMesh & mesh,
298 : OutputDimension output_dimension)
299 : {
300 41981 : switch (output_dimension)
301 : {
302 41970 : case OutputDimension::DEFAULT:
303 : // If the mesh_dimension is 1, we need to write out as 3D.
304 : //
305 : // This works around an issue in Paraview where 1D meshes cannot
306 : // not be visualized correctly. Otherwise, write out based on the effectiveSpatialDimension.
307 41970 : if (mesh.getMesh().mesh_dimension() == 1)
308 4342 : exodus_io.write_as_dimension(3);
309 : else
310 37628 : exodus_io.write_as_dimension(static_cast<int>(mesh.effectiveSpatialDimension()));
311 41970 : break;
312 :
313 11 : case OutputDimension::ONE:
314 : case OutputDimension::TWO:
315 : case OutputDimension::THREE:
316 11 : exodus_io.write_as_dimension(static_cast<int>(output_dimension));
317 11 : break;
318 :
319 0 : case OutputDimension::PROBLEM_DIMENSION:
320 0 : exodus_io.use_mesh_dimension_instead_of_spatial_dimension(true);
321 0 : break;
322 :
323 0 : default:
324 0 : ::mooseError("Unknown output_dimension in Exodus writer");
325 : }
326 41981 : }
327 :
328 : void
329 149921 : Exodus::outputNodalVariables()
330 : {
331 : // Set the output variable to the nodal variables
332 149921 : std::vector<std::string> nodal(getNodalVariableOutput().begin(), getNodalVariableOutput().end());
333 149921 : _exodus_io_ptr->set_output_variables(nodal);
334 :
335 : // Check if the mesh is contiguously numbered, because exodus output will renumber to force that
336 149921 : const auto & mesh = _problem_ptr->mesh().getMesh();
337 : const bool mesh_contiguous_numbering =
338 149921 : (mesh.n_nodes() == mesh.max_node_id()) && (mesh.n_elem() == mesh.max_elem_id());
339 :
340 : // Write the data via libMesh::ExodusII_IO
341 149921 : if (_discontinuous)
342 110 : _exodus_io_ptr->write_timestep_discontinuous(
343 110 : filename(), *_es_ptr, _exodus_num, getOutputTime() + _app.getGlobalTimeOffset());
344 : else
345 299732 : _exodus_io_ptr->write_timestep(
346 299732 : filename(), *_es_ptr, _exodus_num, getOutputTime() + _app.getGlobalTimeOffset());
347 :
348 149921 : if (!_overwrite)
349 146063 : _exodus_num++;
350 :
351 149921 : if (!mesh_contiguous_numbering)
352 36 : handleExodusIOMeshRenumbering();
353 :
354 : // This satisfies the initialization of the ExodusII_IO object
355 149921 : _exodus_initialized = true;
356 149921 : }
357 :
358 : void
359 34713 : Exodus::outputElementalVariables()
360 : {
361 : // Make sure the the file is ready for writing of elemental data
362 34713 : if (!_exodus_initialized || !hasNodalVariableOutput())
363 7611 : outputEmptyTimestep();
364 :
365 : // Write the elemental data
366 34713 : std::vector<std::string> elemental(getElementalVariableOutput().begin(),
367 69426 : getElementalVariableOutput().end());
368 34713 : _exodus_io_ptr->set_output_variables(elemental);
369 34713 : _exodus_io_ptr->write_element_data(*_es_ptr);
370 34713 : }
371 :
372 : void
373 36090 : Exodus::outputPostprocessors()
374 : {
375 : // List of desired postprocessor outputs
376 36090 : const std::set<std::string> & pps = getPostprocessorOutput();
377 :
378 : // Append the postprocessor data to the global name value parameters; scalar outputs
379 : // also append these member variables
380 101390 : for (const auto & name : pps)
381 : {
382 65300 : _global_names.push_back(name);
383 65300 : _global_values.push_back(_problem_ptr->getPostprocessorValueByName(name));
384 : }
385 36090 : }
386 :
387 : void
388 547 : Exodus::outputReporters()
389 : {
390 2126 : for (const auto & combined_name : getReporterOutput())
391 : {
392 1579 : ReporterName r_name(combined_name);
393 1784 : if (_reporter_data.hasReporterValue<Real>(r_name) &&
394 1784 : !hasPostprocessorByName(r_name.getObjectName()))
395 : {
396 205 : const Real & value = _reporter_data.getReporterValue<Real>(r_name);
397 205 : _global_names.push_back(r_name.getValueName());
398 205 : _global_values.push_back(value);
399 : }
400 1579 : }
401 547 : }
402 :
403 : void
404 3775 : Exodus::outputScalarVariables()
405 : {
406 : // List of desired scalar outputs
407 3775 : const std::set<std::string> & out = getScalarOutput();
408 :
409 : // Append the scalar to the global output lists
410 10393 : for (const auto & out_name : out)
411 : {
412 : // Make sure scalar values are in sync with the solution vector
413 : // and are visible on this processor. See TableOutput.C for
414 : // TableOutput::outputScalarVariables() explanatory comments
415 :
416 6618 : MooseVariableScalar & scalar_var = _problem_ptr->getScalarVariable(0, out_name);
417 6618 : scalar_var.reinit();
418 6618 : VariableValue value(scalar_var.sln());
419 :
420 6618 : const std::vector<dof_id_type> & dof_indices = scalar_var.dofIndices();
421 6618 : const unsigned int n = dof_indices.size();
422 6618 : value.resize(n);
423 :
424 6618 : const DofMap & dof_map = scalar_var.sys().dofMap();
425 14237 : for (unsigned int i = 0; i != n; ++i)
426 : {
427 7619 : const processor_id_type pid = dof_map.dof_owner(dof_indices[i]);
428 7619 : this->comm().broadcast(value[i], pid);
429 : }
430 :
431 : // If the scalar has a single component, output the name directly
432 6618 : if (n == 1)
433 : {
434 6104 : _global_names.push_back(out_name);
435 6104 : _global_values.push_back(value[0]);
436 : }
437 :
438 : // If the scalar as many components add indices to the end of the name
439 : else
440 : {
441 2029 : for (unsigned int i = 0; i < n; ++i)
442 : {
443 1515 : std::ostringstream os;
444 1515 : os << out_name << "_" << i;
445 1515 : _global_names.push_back(os.str());
446 1515 : _global_values.push_back(value[i]);
447 1515 : }
448 : }
449 6618 : }
450 3775 : }
451 :
452 : void
453 33608 : Exodus::outputInput()
454 : {
455 : // Format the input file
456 33608 : ExodusFormatter syntax_formatter;
457 33608 : syntax_formatter.printInputFile(_app.actionWarehouse());
458 33608 : syntax_formatter.format();
459 :
460 : // Store the information
461 33608 : _input_record = syntax_formatter.getInputFileRecord();
462 33608 : }
463 :
464 : void
465 163718 : Exodus::output()
466 : {
467 : // Prepare the ExodusII_IO object
468 163718 : outputSetup();
469 163718 : LockFile lf(filename(), processor_id() == 0);
470 :
471 : // Adjust the position of the output
472 163718 : if (_app.hasOutputPosition())
473 6546 : _exodus_io_ptr->set_coordinate_offset(_app.getOutputPosition());
474 :
475 : // Clear the global variables (postprocessors and scalars)
476 163718 : _global_names.clear();
477 163718 : _global_values.clear();
478 :
479 : // Call the individual output methods
480 163718 : AdvancedOutput::output();
481 :
482 : // Write the global variables (populated by the output methods)
483 163718 : if (!_global_values.empty())
484 : {
485 37071 : if (!_exodus_initialized)
486 40 : outputEmptyTimestep();
487 37071 : _exodus_io_ptr->write_global_data(_global_values, _global_names);
488 : }
489 :
490 : // Write the input file record if it exists and the output file is initialized
491 163718 : if (!_input_record.empty() && _exodus_initialized)
492 : {
493 33575 : _exodus_io_ptr->write_information_records(_input_record);
494 33575 : _input_record.clear();
495 : }
496 :
497 : // Reset the mesh changed flag
498 163718 : _exodus_mesh_changed = false;
499 :
500 : // It is possible to have an empty file created with the following scenario. By default the
501 : // 'execute_on_input' flag is setup to run on INITIAL. If the 'execute_on' is set to FINAL
502 : // but the simulation stops early (e.g., --test-checkpoint-half-transient) the Exodus file is
503 : // created but there is no data in it, because of the initial call to write the input data seems
504 : // to create the file but doesn't actually write the data into the solution/mesh is also supplied
505 : // to the IO object. Then if --recover is used this empty file fails to open for appending.
506 : //
507 : // The code below will delete any empty files that exist. Another solution is to set the
508 : // 'execute_on_input' flag to NONE.
509 163718 : std::string current = filename();
510 284441 : if (processor_id() == 0 && MooseUtils::checkFileReadable(current, false, false) &&
511 120723 : (MooseUtils::fileSize(current) == 0))
512 : {
513 3634 : int err = std::remove(current.c_str());
514 3634 : if (err != 0)
515 0 : mooseError("MOOSE failed to remove the empty file ", current);
516 : }
517 163718 : }
518 :
519 : std::string
520 521486 : Exodus::filename()
521 : {
522 : // Append the .e extension on the base file name
523 521486 : std::ostringstream output;
524 521486 : output << _file_base + ".e";
525 :
526 : // Add the -s00x extension to the file
527 521486 : if (_file_num > 1)
528 16416 : output << "-s" << std::setw(_padding) << std::setprecision(0) << std::setfill('0') << std::right
529 16416 : << _file_num;
530 :
531 1042972 : return output.str();
532 521486 : }
533 :
534 : void
535 7651 : Exodus::outputEmptyTimestep()
536 : {
537 : // Check if the mesh is contiguously numbered, because exodus output will renumber to force that
538 7651 : const auto & mesh = _problem_ptr->mesh().getMesh();
539 : const bool mesh_contiguous_numbering =
540 7651 : (mesh.n_nodes() == mesh.max_node_id()) && (mesh.n_elem() == mesh.max_elem_id());
541 :
542 : // Write a timestep with no variables
543 7651 : _exodus_io_ptr->set_output_variables(std::vector<std::string>());
544 15302 : _exodus_io_ptr->write_timestep(
545 15302 : filename(), *_es_ptr, _exodus_num, getOutputTime() + _app.getGlobalTimeOffset());
546 :
547 7651 : if (!_overwrite)
548 7585 : _exodus_num++;
549 :
550 7651 : if (!mesh_contiguous_numbering)
551 21 : handleExodusIOMeshRenumbering();
552 7651 : _exodus_initialized = true;
553 7651 : }
554 :
555 : void
556 2567 : Exodus::clear()
557 : {
558 2567 : _exodus_io_ptr.reset();
559 2567 : }
560 :
561 : void
562 57 : Exodus::handleExodusIOMeshRenumbering()
563 : {
564 : // We renumbered our mesh, so we need the other mesh to do the same
565 57 : if (auto * const disp_problem = _problem_ptr->getDisplacedProblem().get(); disp_problem)
566 : {
567 11 : auto & disp_eq = disp_problem->es();
568 11 : auto & other_mesh = &disp_eq == _es_ptr ? _problem_ptr->mesh().getMesh() : disp_eq.get_mesh();
569 : mooseAssert(
570 : !other_mesh.allow_renumbering(),
571 : "The only way we shouldn't have contiguous numbering is if we've disabled renumbering");
572 11 : other_mesh.allow_renumbering(true);
573 11 : other_mesh.renumber_nodes_and_elements();
574 : // Copying over the comment in MeshOutput::write_equation_systems
575 : // Not sure what good going back to false will do here, the
576 : // renumbering horses have already left the barn...
577 11 : other_mesh.allow_renumbering(false);
578 : }
579 :
580 : // Objects that depend on element/node ids are no longer valid
581 57 : _problem_ptr->meshChanged(
582 : /*intermediate_change=*/false, /*contract_mesh=*/false, /*clean_refinement_flags=*/false);
583 57 : }
|