DataTransferKit - Multiphysics Solution Transfer Services  2.0
DTK_L2ProjectionOperator.cpp
1 //---------------------------------------------------------------------------//
2 /*
3  Copyright (c) 2012, Stuart R. Slattery
4  All rights reserved.
5 
6  Redistribution and use in source and binary forms, with or without
7  modification, are permitted provided that the following conditions are
8  met:
9 
10  *: Redistributions of source code must retain the above copyright
11  notice, this list of conditions and the following disclaimer.
12 
13  *: Redistributions in binary form must reproduce the above copyright
14  notice, this list of conditions and the following disclaimer in the
15  documentation and/or other materials provided with the distribution.
16 
17  *: Neither the name of the University of Wisconsin - Madison nor the
18  names of its contributors may be used to endorse or promote products
19  derived from this software without specific prior written permission.
20 
21  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
24  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
25  HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
28  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
29  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 */
33 //---------------------------------------------------------------------------//
39 //---------------------------------------------------------------------------//
40 
41 #include <algorithm>
42 #include <map>
43 
44 #include "DTK_BasicEntityPredicates.hpp"
45 #include "DTK_DBC.hpp"
46 #include "DTK_IntegrationPoint.hpp"
47 #include "DTK_L2ProjectionOperator.hpp"
48 #include "DTK_ParallelSearch.hpp"
49 #include "DTK_PredicateComposition.hpp"
50 
51 #include <Teuchos_OrdinalTraits.hpp>
52 
53 #include <Teuchos_XMLParameterListCoreHelpers.hpp>
54 
55 #include <Tpetra_Distributor.hpp>
56 
57 #include <BelosPseudoBlockCGSolMgr.hpp>
58 
59 #include <Thyra_DefaultMultipliedLinearOp.hpp>
60 #include <Thyra_LinearOpWithSolveFactoryHelpers.hpp>
61 #include <Thyra_TpetraThyraWrappers.hpp>
62 
63 #include <Stratimikos_DefaultLinearSolverBuilder.hpp>
64 
65 namespace DataTransferKit
66 {
67 //---------------------------------------------------------------------------//
68 // Constructor.
70  const Teuchos::RCP<const TpetraMap> &domain_map,
71  const Teuchos::RCP<const TpetraMap> &range_map,
72  const Teuchos::ParameterList &parameters )
73  : Base( domain_map, range_map )
74 {
75  // Get the integration order.
76  const Teuchos::ParameterList &l2_list =
77  parameters.sublist( "L2 Projection" );
78  DTK_REQUIRE( l2_list.isParameter( "Integration Order" ) );
79  d_int_order = l2_list.get<int>( "Integration Order" );
80 
81  // Get the search list.
82  d_search_list = parameters.sublist( "Search" );
83 }
84 
85 //---------------------------------------------------------------------------//
86 // Setup the map operator.
87 void L2ProjectionOperator::setupImpl(
88  const Teuchos::RCP<FunctionSpace> &domain_space,
89  const Teuchos::RCP<FunctionSpace> &range_space )
90 {
91  DTK_REQUIRE( Teuchos::nonnull( domain_space ) );
92  DTK_REQUIRE( Teuchos::nonnull( range_space ) );
93 
94  // Get the parallel communicator.
95  Teuchos::RCP<const Teuchos::Comm<int>> comm =
96  this->getDomainMap()->getComm();
97 
98  // Determine if we have range and domain data on this process.
99  bool nonnull_domain = Teuchos::nonnull( domain_space->entitySet() );
100  bool nonnull_range = Teuchos::nonnull( range_space->entitySet() );
101 
102  // Get an iterator over the domain entities.
103  EntityIterator domain_iterator;
104  if ( nonnull_domain )
105  {
106  LocalEntityPredicate local_predicate(
107  domain_space->entitySet()->communicator()->getRank() );
108  PredicateFunction domain_predicate = PredicateComposition::And(
109  domain_space->selectFunction(), local_predicate.getFunction() );
110  domain_iterator = domain_space->entitySet()->entityIterator(
111  domain_space->entitySet()->physicalDimension(), domain_predicate );
112  }
113 
114  // Get an iterator over the range entities.
115  EntityIterator range_iterator;
116  if ( nonnull_range )
117  {
118  LocalEntityPredicate local_predicate(
119  range_space->entitySet()->communicator()->getRank() );
120  PredicateFunction range_predicate = PredicateComposition::And(
121  range_space->selectFunction(), local_predicate.getFunction() );
122  range_iterator = range_space->entitySet()->entityIterator(
123  range_space->entitySet()->physicalDimension(), range_predicate );
124  }
125 
126  // Assemble the mass matrix over the range entity set.
127  Teuchos::RCP<Tpetra::CrsMatrix<Scalar, LO, GO>> mass_matrix;
128  Teuchos::RCP<IntegrationPointSet> range_ip_set;
129  assembleMassMatrix( range_space, range_iterator, mass_matrix,
130  range_ip_set );
131 
132  // Assemble the coupling matrix.
133  Teuchos::RCP<Tpetra::CrsMatrix<Scalar, LO, GO>> coupling_matrix;
134  assembleCouplingMatrix( domain_space, domain_iterator, range_ip_set,
135  coupling_matrix );
136 
137  // Create an abstract wrapper for the mass matrix.
138  Teuchos::RCP<const Thyra::VectorSpaceBase<double>>
139  thyra_range_vector_space_M =
140  Thyra::createVectorSpace<double>( mass_matrix->getRangeMap() );
141  Teuchos::RCP<const Thyra::VectorSpaceBase<double>>
142  thyra_domain_vector_space_M =
143  Thyra::createVectorSpace<double>( mass_matrix->getDomainMap() );
144  Teuchos::RCP<const Thyra::TpetraLinearOp<Scalar, LO, GO>> thyra_M =
145  Teuchos::rcp( new Thyra::TpetraLinearOp<Scalar, LO, GO>() );
146  Teuchos::rcp_const_cast<Thyra::TpetraLinearOp<Scalar, LO, GO>>( thyra_M )
147  ->constInitialize( thyra_range_vector_space_M,
148  thyra_domain_vector_space_M, mass_matrix );
149 
150  // Create an abstract wrapper for the coupling matrix.
151  Teuchos::RCP<const Thyra::VectorSpaceBase<double>>
152  thyra_range_vector_space_A =
153  Thyra::createVectorSpace<double>( coupling_matrix->getRangeMap() );
154  Teuchos::RCP<const Thyra::VectorSpaceBase<double>>
155  thyra_domain_vector_space_A =
156  Thyra::createVectorSpace<double>( coupling_matrix->getDomainMap() );
157  Teuchos::RCP<const Thyra::TpetraLinearOp<Scalar, LO, GO>> thyra_A =
158  Teuchos::rcp( new Thyra::TpetraLinearOp<Scalar, LO, GO>() );
159  Teuchos::rcp_const_cast<Thyra::TpetraLinearOp<Scalar, LO, GO>>( thyra_A )
160  ->constInitialize( thyra_range_vector_space_A,
161  thyra_domain_vector_space_A, coupling_matrix );
162 
163  // Create the inverse of the mass matrix. Use the conjugate gradient
164  // method to invert the SPD mass matrix.
165  Teuchos::RCP<Teuchos::ParameterList> builder_params =
166  Teuchos::parameterList( "Stratimikos" );
167 
168  builder_params->set( "Linear Solver Type", "Belos" );
169  builder_params->set( "Preconditioner Type", "None" );
170 
171  auto &linear_solver_types_list =
172  builder_params->sublist( "Linear Solver Types" );
173  auto &belos_list = linear_solver_types_list.sublist( "Belos" );
174  belos_list.set( "Solver Type", "Pseudo Block CG" );
175  auto &solver_types_list = belos_list.sublist( "Solver Types" );
176  auto &cg_list = solver_types_list.sublist( "Pseudo Block CG" );
177  cg_list.set( "Convergence Tolerance", 1.0e-10 );
178  cg_list.set( "Verbosity", Belos::Errors + Belos::Warnings +
179  Belos::TimingDetails + Belos::FinalSummary +
180  Belos::StatusTestDetails );
181  cg_list.set( "Output Frequency", 1 );
182 
183  Stratimikos::DefaultLinearSolverBuilder builder;
184  builder.setParameterList( builder_params );
185  Teuchos::RCP<Thyra::LinearOpWithSolveFactoryBase<double>> factory =
186  Thyra::createLinearSolveStrategy( builder );
187  Teuchos::RCP<const Thyra::LinearOpBase<double>> thyra_M_inv =
188  Thyra::inverse<double>( *factory, thyra_M );
189 
190  // Create the projection operator: Op = M^-1 * A.
191  d_l2_operator = Thyra::multiply<double>( thyra_M_inv, thyra_A );
192  DTK_ENSURE( Teuchos::nonnull( d_l2_operator ) );
193 }
194 
195 //---------------------------------------------------------------------------//
196 // Apply the operator.
197 void L2ProjectionOperator::applyImpl( const TpetraMultiVector &X,
198  TpetraMultiVector &Y,
199  Teuchos::ETransp mode, double alpha,
200  double beta ) const
201 {
202  DTK_REQUIRE( Teuchos::NO_TRANS == mode );
203  Teuchos::RCP<const Thyra::MultiVectorBase<double>> thyra_X =
204  Thyra::createConstMultiVector<double>( Teuchos::rcpFromRef( X ) );
205  Teuchos::RCP<Thyra::MultiVectorBase<double>> thyra_Y =
206  Thyra::createMultiVector<double>( Teuchos::rcpFromRef( Y ) );
207  d_l2_operator->apply( Thyra::NOTRANS, *thyra_X, thyra_Y.ptr(), alpha,
208  beta );
209 }
210 
211 //---------------------------------------------------------------------------//
212 // Transpose apply option.
213 bool L2ProjectionOperator::hasTransposeApplyImpl() const { return false; }
214 
215 //---------------------------------------------------------------------------//
216 // Assemble the mass matrix and range integration point set.
217 void L2ProjectionOperator::assembleMassMatrix(
218  const Teuchos::RCP<FunctionSpace> &range_space,
219  EntityIterator range_iterator,
220  Teuchos::RCP<Tpetra::CrsMatrix<Scalar, LO, GO>> &mass_matrix,
221  Teuchos::RCP<IntegrationPointSet> &range_ip_set )
222 {
223  // Initialize output variables.
224  Teuchos::RCP<const Teuchos::Comm<int>> range_comm =
225  range_space->entitySet()->communicator();
226  mass_matrix =
227  Tpetra::createCrsMatrix<Scalar, LO, GO>( this->getRangeMap() );
228  range_ip_set = Teuchos::rcp( new IntegrationPointSet( range_comm ) );
229 
230  // Get function space objects.
231  Teuchos::RCP<EntityLocalMap> range_local_map = range_space->localMap();
232  Teuchos::RCP<EntityShapeFunction> range_shape_function =
233  range_space->shapeFunction();
234  Teuchos::RCP<EntityIntegrationRule> range_integration_rule =
235  range_space->integrationRule();
236 
237  // Initialize data for assembly loop.
238  double range_entity_measure = 0.0;
239  IntegrationPoint range_ip;
240  range_ip.d_physical_coordinates.resize(
241  range_space->entitySet()->physicalDimension() );
242  Teuchos::Array<Teuchos::Array<double>> int_points;
243  Teuchos::Array<double> int_weights;
244  Teuchos::Array<Teuchos::Array<double>> shape_evals;
245  Teuchos::Array<SupportId> range_support_ids;
246  Teuchos::Array<double> mm_values;
247  int range_cardinality = 0;
248  int num_ip = 0;
249  double temp = 0;
250 
251  // Assemble the mass matrix and integration point set.
252  EntityIterator range_it;
253  EntityIterator range_begin = range_iterator.begin();
254  EntityIterator range_end = range_iterator.end();
255  for ( range_it = range_begin; range_it != range_end; ++range_it )
256  {
257  // Get the support ids of the entity.
258  range_shape_function->entitySupportIds( *range_it, range_support_ids );
259 
260  // Get the measure of the entity.
261  range_entity_measure = range_local_map->measure( *range_it );
262 
263  // Get the integration rule.
264  range_integration_rule->getIntegrationRule( *range_it, d_int_order,
265  int_points, int_weights );
266 
267  // Create new integration points.
268  num_ip = int_weights.size();
269  shape_evals.resize( num_ip );
270  for ( int p = 0; p < num_ip; ++p )
271  {
272  // Add owner data.
273  range_ip.d_owner_measure = range_entity_measure;
274  range_ip.d_owner_support_ids = range_support_ids;
275  range_ip.d_integration_weight = int_weights[p];
276 
277  // Evaluate the shape function.
278  range_shape_function->evaluateValue( *range_it, int_points[p](),
279  shape_evals[p] );
280  range_ip.d_owner_shape_evals = shape_evals[p];
281 
282  // Map the integration point to the physical frame of the range
283  // entity.
284  range_local_map->mapToPhysicalFrame(
285  *range_it, int_points[p](), range_ip.d_physical_coordinates() );
286 
287  // Add the integration point to the set.
288  range_ip_set->addPoint( range_ip );
289  }
290 
291  // Fill the mass matrix.
292  range_cardinality = range_support_ids.size();
293  for ( int ni = 0; ni < range_cardinality; ++ni )
294  {
295  mm_values.assign( range_cardinality, 0.0 );
296  for ( int p = 0; p < num_ip; ++p )
297  {
298  temp =
299  range_entity_measure * int_weights[p] * shape_evals[p][ni];
300  for ( int nj = 0; nj < range_cardinality; ++nj )
301  {
302  mm_values[nj] += temp * shape_evals[p][nj];
303  }
304  }
305  mass_matrix->insertGlobalValues( range_support_ids[ni],
306  range_support_ids(), mm_values() );
307  }
308  }
309 
310  // Finalize the mass matrix.
311  mass_matrix->fillComplete( this->getRangeMap(), this->getRangeMap() );
312  DTK_CHECK( mass_matrix->isFillComplete() );
313 
314  // Finalize the integration point set.
315  range_ip_set->finalize();
316 }
317 
318 //---------------------------------------------------------------------------//
319 void L2ProjectionOperator::assembleCouplingMatrix(
320  const Teuchos::RCP<FunctionSpace> &domain_space,
321  EntityIterator domain_iterator,
322  const Teuchos::RCP<IntegrationPointSet> &range_ip_set,
323  Teuchos::RCP<Tpetra::CrsMatrix<Scalar, LO, GO>> &coupling_matrix )
324 {
325  // Get the parallel communicator.
326  Teuchos::RCP<const Teuchos::Comm<int>> comm =
327  domain_space->entitySet()->communicator();
328 
329  // Get the physical dimension.
330  int physical_dimension = domain_space->entitySet()->physicalDimension();
331 
332  // Build a parallel search over the domain.
333  ParallelSearch psearch( comm, physical_dimension, domain_iterator,
334  domain_space->localMap(), d_search_list );
335 
336  // Search the domain with the range integration point set.
337  EntityIterator ip_iterator = range_ip_set->entityIterator();
338  psearch.search( ip_iterator, range_ip_set, d_search_list );
339 
340  // Extract the set of local range entities that were found in domain
341  // entities.
342  int global_max_support = range_ip_set->globalMaxSupportSize();
343  int ip_stride = global_max_support + 1;
344  Teuchos::Array<int> export_ranks;
345  Teuchos::Array<EntityId> export_ip_domain_ids;
346  Teuchos::Array<double> export_measures_weights;
347  Teuchos::Array<double> export_shape_evals;
348  Teuchos::Array<SupportId> export_support_ids;
349  Teuchos::Array<EntityId> domain_ids;
350  Teuchos::Array<EntityId>::const_iterator domain_id_it;
351  EntityIterator ip_it;
352  EntityIterator ip_begin = ip_iterator.begin();
353  EntityIterator ip_end = ip_iterator.end();
354  int num_support = 0;
355  for ( ip_it = ip_begin; ip_it != ip_end; ++ip_it )
356  {
357  // Get the domain entities in which the integration point was found.
358  psearch.getDomainEntitiesFromRange( ip_it->id(), domain_ids );
359 
360  // Get the current integration point.
361  const IntegrationPoint &current_ip =
362  range_ip_set->getPoint( ip_it->id() );
363 
364  // For each supporting domain entity, pair the integration point id
365  // and its support id.
366  for ( domain_id_it = domain_ids.begin();
367  domain_id_it != domain_ids.end(); ++domain_id_it )
368  {
369  // Add the domain rank.
370  export_ranks.push_back(
371  psearch.domainEntityOwnerRank( *domain_id_it ) );
372 
373  // Add the ip-domain id pair.
374  export_ip_domain_ids.push_back( ip_it->id() );
375  export_ip_domain_ids.push_back( *domain_id_it );
376 
377  // Add the ip weights times measures scaled by the number of
378  // domains in which ip was found.
379  export_measures_weights.push_back( current_ip.d_owner_measure *
380  current_ip.d_integration_weight /
381  domain_ids.size() );
382 
383  // Add the ip shape function evals and support ids with padding.
384  num_support = current_ip.d_owner_support_ids.size();
385  export_shape_evals.push_back( num_support );
386  export_support_ids.push_back( num_support );
387  for ( int i = 0; i < num_support; ++i )
388  {
389  export_shape_evals.push_back(
390  current_ip.d_owner_shape_evals[i] );
391  export_support_ids.push_back(
392  current_ip.d_owner_support_ids[i] );
393  }
394  for ( int i = num_support; i < global_max_support; ++i )
395  {
396  export_shape_evals.push_back( 0.0 );
397  export_support_ids.push_back( 0 );
398  }
399  }
400  }
401 
402  // Communicate the integration points to the domain parallel
403  // decomposition.
404  Tpetra::Distributor range_to_domain_dist( comm );
405  int num_import = range_to_domain_dist.createFromSends( export_ranks() );
406  Teuchos::Array<EntityId> import_ip_domain_ids( 2 * num_import );
407  range_to_domain_dist.doPostsAndWaits( export_ip_domain_ids().getConst(), 2,
408  import_ip_domain_ids() );
409  Teuchos::Array<double> import_measures_weights( num_import );
410  range_to_domain_dist.doPostsAndWaits( export_measures_weights().getConst(),
411  1, import_measures_weights() );
412  Teuchos::Array<double> import_shape_evals( ip_stride * num_import );
413  range_to_domain_dist.doPostsAndWaits( export_shape_evals().getConst(),
414  ip_stride, import_shape_evals() );
415  Teuchos::Array<SupportId> import_support_ids( ip_stride * num_import );
416  range_to_domain_dist.doPostsAndWaits( export_support_ids().getConst(),
417  ip_stride, import_support_ids() );
418 
419  // Map the ip-domain pairs to a local id.
420  std::map<std::pair<EntityId, EntityId>, int> ip_domain_lid_map;
421  std::pair<EntityId, EntityId> lid_pair;
422  for ( int n = 0; n < num_import; ++n )
423  {
424  lid_pair.first = import_ip_domain_ids[2 * n];
425  lid_pair.second = import_ip_domain_ids[2 * n + 1];
426  ip_domain_lid_map[lid_pair] = n;
427  }
428 
429  // Cleanup before filling the matrix.
430  import_ip_domain_ids.clear();
431  export_ranks.clear();
432  export_ip_domain_ids.clear();
433  export_measures_weights.clear();
434  export_shape_evals.clear();
435  export_support_ids.clear();
436 
437  // Allocate the coupling matrix.
438  coupling_matrix =
439  Tpetra::createCrsMatrix<Scalar, LO, GO>( this->getRangeMap() );
440 
441  // Construct the entries of the coupling matrix.
442  Teuchos::Array<EntityId> ip_entity_ids;
443  Teuchos::Array<EntityId>::const_iterator ip_entity_id_it;
444  Teuchos::ArrayView<const double> ip_parametric_coords;
445  Teuchos::Array<double> domain_shape_values;
446  Teuchos::Array<double> cm_values;
447  Teuchos::Array<double>::iterator domain_shape_it;
448  Teuchos::Array<GO> domain_support_ids;
449  EntityIterator domain_it;
450  EntityIterator domain_begin = domain_iterator.begin();
451  EntityIterator domain_end = domain_iterator.end();
452  int local_id = 0;
453  int range_cardinality = 0;
454  int domain_cardinality = 0;
455  double temp = 0.0;
456  int ip_index;
457  for ( domain_it = domain_begin; domain_it != domain_end; ++domain_it )
458  {
459  // Get the domain Support ids supporting the domain entity.
460  domain_space->shapeFunction()->entitySupportIds( *domain_it,
461  domain_support_ids );
462 
463  // Get the integration points that mapped into this domain entity.
464  psearch.getRangeEntitiesFromDomain( domain_it->id(), ip_entity_ids );
465 
466  // Sum into the global coupling matrix row at each integration point.
467  for ( ip_entity_id_it = ip_entity_ids.begin();
468  ip_entity_id_it != ip_entity_ids.end(); ++ip_entity_id_it )
469  {
470  // Get the local data id.
471  lid_pair.first = *ip_entity_id_it;
472  lid_pair.second = domain_it->id();
473  DTK_CHECK( ip_domain_lid_map.count( lid_pair ) );
474  local_id = ip_domain_lid_map.find( lid_pair )->second;
475 
476  // Get the parametric coordinates of the integration point in the
477  // domain entity.
478  psearch.rangeParametricCoordinatesInDomain(
479  domain_it->id(), *ip_entity_id_it, ip_parametric_coords );
480 
481  // Evaluate the shape function at the integration point
482  // parametric coordinates.
483  domain_space->shapeFunction()->evaluateValue(
484  *domain_it, ip_parametric_coords, domain_shape_values );
485  DTK_CHECK( domain_shape_values.size() ==
486  domain_support_ids.size() );
487 
488  // Fill the coupling matrix.
489  range_cardinality = import_shape_evals[ip_stride * local_id];
490  domain_cardinality = domain_shape_values.size();
491  cm_values.assign( domain_cardinality, 0.0 );
492  for ( int i = 0; i < range_cardinality; ++i )
493  {
494  ip_index = ip_stride * local_id + i + 1;
495  temp = import_measures_weights[local_id] *
496  import_shape_evals[ip_index];
497  for ( int j = 0; j < domain_cardinality; ++j )
498  {
499  cm_values[j] = temp * domain_shape_values[j];
500  }
501  coupling_matrix->insertGlobalValues(
502  import_support_ids[ip_index], domain_support_ids(),
503  cm_values() );
504  }
505  }
506  }
507 
508  // Finalize the coupling matrix.
509  coupling_matrix->fillComplete( this->getDomainMap(), this->getRangeMap() );
510 }
511 
512 //---------------------------------------------------------------------------//
513 
514 } // end namespace DataTransferKit
515 
516 //---------------------------------------------------------------------------//
517 // end DTK_L2ProjectionOperator.cpp
518 //---------------------------------------------------------------------------//
Entity iterator interface.
Assertions and Design-by-Contract for error handling.
std::function< bool(Entity)> PredicateFunction
Predicate function typedef.
Definition: DTK_Types.hpp:64
L2ProjectionOperator(const Teuchos::RCP< const TpetraMap > &domain_map, const Teuchos::RCP< const TpetraMap > &range_map, const Teuchos::ParameterList &parameters)
Constructor.
void applyImpl(const TpetraMultiVector &X, TpetraMultiVector &Y, Teuchos::ETransp mode=Teuchos::NO_TRANS, double alpha=Teuchos::ScalarTraits< double >::one(), double beta=Teuchos::ScalarTraits< double >::zero()) const override
Apply the operator.
DTK_BasicEntitySet.cpp.
EntityId id() const
Client interface.
Definition: DTK_Entity.cpp:82