Loading [MathJax]/extensions/tex2jax.js

DUNE-FEM (unstable)

tuplemapper.hh
1#ifndef DUNE_FEM_SPACE_COMBINEDSPACE_TUPLEMAPPER_HH
2#define DUNE_FEM_SPACE_COMBINEDSPACE_TUPLEMAPPER_HH
3
4#include <array>
5#include <tuple>
6#include <utility>
7
8#include <dune/common/hybridutilities.hh>
9
10#include <dune/fem/common/utility.hh>
11#include <dune/fem/space/common/adaptationmanager.hh>
12#include <dune/fem/space/common/dofmanager.hh>
13#include <dune/fem/space/mapper/dofmapper.hh>
14#include <dune/fem/space/mapper/nonblockmapper.hh>
15#include <dune/fem/misc/functor.hh>
16
17#if __GNUC__ >= 13
18// save diagnostic state
19#pragma GCC diagnostic push
20// turn off the specific warning, caused by code in line 419.
21#pragma GCC diagnostic ignored "-Wattributes"
22#endif
23
24
25namespace Dune
26{
27
28 namespace Fem
29 {
30
31 // Internal forward declaration
32 // ----------------------------
33 template< class GridPart, class ... Mapper >
34 class TupleMapper;
35
36
37#ifndef DOXYGEN
38
39 namespace __TupleMapper
40 {
41
42 // Traits
43 // ------
44
45 template< class GridPart, class ... Mapper >
46 struct Traits
47 {
48 static_assert( Std::are_all_same< typename Mapper::ElementType ... >::value,
49 "TupleMapper needs common ElementType" );
50
51 typedef typename std::tuple_element< 0, std::tuple< Mapper ... > >::type FirstMapperType;
52 typedef typename FirstMapperType::ElementType ElementType;
53 typedef typename FirstMapperType::SizeType SizeType;
54 typedef typename FirstMapperType::GlobalKeyType GlobalKeyType;
55
56 typedef TupleMapper< GridPart, Mapper ... > DofMapperType;
57 };
58
59 // CombinedIndex
60 // -------------
61
62 template< class Index, class Int, Int i >
63 struct CombinedIndex
64 {
65 constexpr CombinedIndex ( Index index, Index offset ) : index_( index ), offset_( offset ) {}
66
67 static constexpr Int component () { return i; }
68
69 constexpr operator Index () const { return index_ + offset_; }
70
71 constexpr Index index () const { return index_; }
72
73 constexpr Index offset () const { return offset_; }
74
75 private:
76 Index index_, offset_;
77 };
78
79
80 // DofMapper
81 // ---------
82
83 template< class T, template< class > class Base = Dune::Fem::DofMapper >
84 class DofMapper;
85
86 template< class GridPart, class ... Mapper, template< class > class Base >
87 class DofMapper< Traits< GridPart, Mapper ... >, Base >
88 : public Base< Traits< GridPart, Mapper ... > >
89 {
90 typedef Base< Traits< GridPart, Mapper ... > > BaseType;
91
92 // FunctorWrapper
93 // --------------
94
95 template< class Functor, int i >
96 struct FunctorWrapper
97 {
98 FunctorWrapper ( Functor functor, int localOffset, int globalOffset )
99 : functor_( functor ),
100 localOffset_( localOffset ),
101 globalOffset_( globalOffset )
102 {}
103
104 template< class GlobalKey >
105 void operator() ( int localDof, const GlobalKey &globalKey ) const
106 {
107 functor_( localDof + localOffset_, CombinedIndex< GlobalKey, int, i >( globalKey, globalOffset_ ) );
108 }
109
110 template< class GlobalKey >
111 void operator() ( const GlobalKey &globalKey ) const
112 {
113 functor_( CombinedIndex< GlobalKey, int, i >( globalKey, globalOffset_ ) );
114 }
115
116 private:
117 Functor functor_;
118 const int localOffset_;
119 const int globalOffset_;
120 };
121
122 protected:
123 // size of the Mapper Tuple
124 static const int mapperTupleSize = sizeof ... ( Mapper );
125
126 typedef std::array< typename BaseType::SizeType, mapperTupleSize + 1 > OffsetType;
127
128 public:
129 typedef typename BaseType::ElementType ElementType;
130 typedef typename BaseType::SizeType SizeType;
131 typedef typename BaseType::Traits::GlobalKeyType GlobalKeyType;
132
133 typedef GridPart GridPartType;
134
135 DofMapper ( GridPartType &gridPart, Mapper & ... mapper )
136 : gridPart_( gridPart ),
137 mapperTuple_( mapper ... )
138 {
139 computeOffset();
140 }
141
142 DofMapper ( GridPartType &gridPart, Mapper && ... mapper )
143 : gridPart_( gridPart ),
144 mapperTuple_( std::move( mapper ) ... )
145 {
146 computeOffset();
147 }
148
149 SizeType size () const { return size( std::index_sequence_for< Mapper ... >() ); }
150
151 bool contains ( const int codim ) const { return contains( codim, std::index_sequence_for< Mapper ... >() ); }
152
153 bool fixedDataSize ( int codim ) const { return fixedDataSize( codim, std::index_sequence_for< Mapper ... >() ); }
154
155 template< class Functor >
156 void mapEach ( const ElementType &element, Functor f ) const
157 {
158 OffsetType localOffset;
159 localOffset[ 0 ] = 0;
160 Hybrid::forEach( std::make_index_sequence< mapperTupleSize >{},
161 [ & ]( auto i )
162 {
163 FunctorWrapper< Functor, i > wrappedFunctor( f, localOffset[ i ], globalOffset_[ i ] );
164 std::get< i >( mapperTuple_ ).mapEach( element, wrappedFunctor );
165 localOffset[ i + 1 ] = localOffset[ i ] + std::get< i >( mapperTuple_ ).numDofs( element );
166 } );
167 }
168
169 template< class Entity, class Functor >
170 void mapEachEntityDof ( const Entity &entity, Functor f ) const
171 {
172 OffsetType localOffset;
173 localOffset[ 0 ] = 0;
174 Hybrid::forEach( std::make_index_sequence< mapperTupleSize >{},
175 [ & ]( auto i )
176 {
177 FunctorWrapper< Functor, i > wrappedFunctor( f, localOffset[ i ], globalOffset_[ i ] );
178 std::get< i >( mapperTuple_ ).mapEachEntityDof( entity, wrappedFunctor );
179 localOffset[ i + 1 ] = localOffset[ i ] + std::get< i >( mapperTuple_ ).numEntityDofs( entity );
180 } );
181 }
182
183 [[deprecated("Use onSubEntity method with char vector instead")]]
184 void onSubEntity ( const ElementType &element, int i, int c, std::vector< bool > &indices ) const
185 {
186 std::vector< char > _idx;
187 onSubEntity(element, i, c, _idx);
188 indices.resize( _idx.size() );
189 for (std::size_t i=0; i<_idx.size();++i)
190 _idx[i] = indices[i] > 0;
191 }
192 // this method returns which local dofs are attached to the given subentity.
193 // indices[locDofNr] =
194 // 0 : not attached, not equal to 0 : attached
195 // (so this method can still be used in the way the deprecated method was).
196 // New: In case the dof can be associated to a component of the
197 // space, the value returned is that component+1. In other
198 // cases (normal velocity for RT for example) the value is -1).
199 // So indices[i] is in [-1,dimRange+1]
200 void onSubEntity ( const ElementType &element, int i, int c, std::vector< char > &indices ) const
201 {
202 indices.resize( numDofs( element ) );
203 OffsetType localOffset;
204 localOffset[ 0 ] = 0;
205 int rangeOffset = 0;
206 Hybrid::forEach( std::make_index_sequence< mapperTupleSize >{},
207 [ & ]( auto t )
208 {
209 std::vector< char > subIndices;
210 std::get< t >( mapperTuple_ ).onSubEntity( element, i,c, subIndices );
211 auto localSize = std::get< t >( mapperTuple_ ).numDofs( element );
212 assert( subIndices.size() == localSize );
213 assert( localOffset[ t ] + localSize <= indices.size() );
214 for (std::size_t d=0;d<subIndices.size();++d)
215 {
216 indices[ localOffset[t] + d ] = subIndices[d]==0? 0 : subIndices[d] + rangeOffset;
217 }
218 // std::copy( subIndices.begin(), subIndices.end(), indices.begin() + localOffset[ t ] );
219 /*
220 std::cout << "\t space<" << t << ">:";
221 for (std::size_t d=0;d<localSize;++d)
222 std::cout << " " << subIndices[d];
223 std::cout << " ->";
224 for (std::size_t d=0;d<indices.size();++d)
225 std::cout << " " << indices[d];
226 std::cout << std::endl;
227 */
228 // FIXME: here we need 'dimRange' of the subspace to cover
229 // cases where a space is vector valued by has blockSize=1
230 // like RT. Otherwise we will get for RTxRT something like
231 // [1,0,0,1, 2,0,0,2] instead of [1,0,0,1, 3,0,0,3]
232 rangeOffset += std::get< t >( mapperTuple_ ).blockSize;
233 localOffset[ t + 1 ] = localOffset[ t ] + localSize;
234 } );
235 /*
236 std::cout << "element (" << i << "," << c << "):";
237 for (std::size_t d=0;d<indices.size();++d)
238 std::cout << " " << indices[d];
239 std::cout << std::endl;
240 */
241 // DUNE_THROW( NotImplemented, "Method onSubEntity(...) not yet implemented for TupleMapper" );
242 }
243
244 int maxNumDofs () const { return maxNumDofs( std::index_sequence_for< Mapper ... >() ); }
245
246 SizeType numDofs ( const ElementType &element ) const { return numDofs( element, std::index_sequence_for< Mapper ... >() ); }
247
248 template< class Entity >
249 SizeType numEntityDofs ( const Entity &entity ) const { return numEntityDofs( entity, std::index_sequence_for< Mapper ... >() ); }
250
251 void map ( const ElementType &element, std::vector< SizeType > &indices ) const
252 {
253 indices.resize( numDofs( element ) );
254 mapEach( element, AssignFunctor< std::vector< SizeType > >( indices ) );
255 }
256 template< class Entity >
257 void mapEntityDofs ( const Entity &entity, std::vector< SizeType > &indices ) const
258 {
259 indices.resize( numEntityDofs( entity ) );
260 mapEachEntityDof( entity, AssignFunctor< std::vector< SizeType > >( indices ) );
261 }
262
263 static constexpr bool consecutive () noexcept { return false; }
264
265 SizeType numBlocks () const
266 {
267 DUNE_THROW( NotImplemented, "Method numBlocks() called on non-adaptive block mapper" );
268 }
269
270 SizeType numberOfHoles ( int ) const
271 {
272 DUNE_THROW( NotImplemented, "Method numberOfHoles() called on non-adaptive block mapper" );
273 }
274
275 GlobalKeyType oldIndex ( int hole, int ) const
276 {
277 DUNE_THROW( NotImplemented, "Method oldIndex() called on non-adaptive block mapper" );
278 }
279
280 GlobalKeyType newIndex ( int hole, int ) const
281 {
282 DUNE_THROW( NotImplemented, "Method newIndex() called on non-adaptive block mapper" );
283 }
284
285 SizeType oldOffSet ( int ) const
286 {
287 DUNE_THROW( NotImplemented, "Method oldOffSet() called on non-adaptive block mapper" );
288 }
289
290 SizeType offSet ( int ) const
291 {
292 DUNE_THROW( NotImplemented, "Method offSet() called on non-adaptive block mapper" );
293 }
294
295 void update()
296 {
297 // compute update for each mapper (if any)
298 Hybrid::forEach( std::make_index_sequence< mapperTupleSize >{},
299 [ & ](auto i){ std::get< i >( mapperTuple_ ).update(); } );
300
301 computeOffset();
302 }
303
304 /*** NonInterface Methods ***/
305
306 SizeType offset ( int i ) const { return globalOffset_[ i ]; }
307
308 template< int i >
309 SizeType subSize () const { return std::get< i >( mapperTuple_ ).size(); }
310
311 protected:
312 template< std::size_t ... i >
313 SizeType size ( std::index_sequence< i ... > ) const
314 {
315 return Std::sum( std::get< i >( mapperTuple_ ).size() ... );
316 }
317
318 template< std::size_t ... i >
319 bool fixedDataSize ( const int codim, std::index_sequence< i ... > ) const
320 {
321 return Std::And( std::get< i >( mapperTuple_ ).fixedDataSize( codim ) ... );
322 }
323
324 template< std::size_t ... i >
325 bool contains ( const int codim, std::index_sequence< i ... > ) const
326 {
327 return Std::Or( std::get< i >( mapperTuple_ ).contains( codim ) ... );
328 }
329
330 template< std::size_t ... i >
331 int maxNumDofs ( std::index_sequence< i ... > ) const
332 {
333 return Std::sum( std::get< i >( mapperTuple_ ).maxNumDofs() ... );
334 }
335
336 template< std::size_t ... i >
337 SizeType numDofs ( const ElementType &element, std::index_sequence< i ... > ) const
338 {
339 return Std::sum( std::get< i >( mapperTuple_ ).numDofs( element ) ... );
340 }
341
342 template< class Entity, std::size_t ... i >
343 SizeType numEntityDofs ( const Entity &entity, std::index_sequence< i ... > ) const
344 {
345 return Std::sum( std::get< i >( mapperTuple_ ).numEntityDofs( entity ) ... );
346 }
347
348 void computeOffset ()
349 {
350 globalOffset_[ 0 ] = 0;
351 // compute new offsets
352 Hybrid::forEach( std::make_index_sequence< mapperTupleSize >{},
353 [ & ]( auto i ){ globalOffset_[ i + 1 ] = globalOffset_[ i ] + std::get< i >( mapperTuple_ ).size(); } );
354 }
355
356 GridPartType &gridPart_;
357 std::tuple< Mapper ... > mapperTuple_;
358 OffsetType globalOffset_;
359 };
360
361
362
363 // AdaptiveDofMapper
364 // -----------------
365
366 template< class T >
367 class AdaptiveDofMapper;
368
369 template< class GridPart, class ... Mapper >
370 class AdaptiveDofMapper< Traits< GridPart, Mapper ... > >
371 : public DofMapper< Traits< GridPart, Mapper ... >, Dune::Fem::AdaptiveDofMapper >
372 {
373 typedef DofMapper< Traits< GridPart, Mapper ... >, Dune::Fem::AdaptiveDofMapper > BaseType;
374
375 protected:
376 typedef typename GridPart :: GridType GridType;
377 typedef AdaptationMethod< GridType > AdaptationMethodType;
378
379 typedef typename BaseType::OffsetType OffsetType;
380
381 using BaseType::mapperTupleSize;
382 using BaseType::mapperTuple_;
383 using BaseType::gridPart_;
384 using BaseType::globalOffset_;
385
386 public:
387 typedef typename BaseType::ElementType ElementType;
388 typedef typename BaseType::SizeType SizeType;
389 typedef typename BaseType::GlobalKeyType GlobalKeyType;
390 typedef GridPart GridPartType;
391
392 AdaptiveDofMapper ( GridPartType &gridPart, Mapper & ... mapper )
393 : BaseType( gridPart, mapper ... ),
394 numBlocks_( computeNumBlocks() ),
395 isCallBackAdapt_( AdaptationMethodType( gridPart.grid() ).isCallBackAdaptation() ),
396 needsFullUpdate_(true)
397 {
398 oldGlobalOffset_ = globalOffset_;
400 }
401
402 AdaptiveDofMapper ( GridPartType &gridPart, Mapper && ... mapper )
403 : BaseType( gridPart, std::move( mapper ) ... ),
404 numBlocks_( computeNumBlocks() ),
405 isCallBackAdapt_( AdaptationMethodType( gridPart.grid() ).isCallBackAdaptation() ),
406 needsFullUpdate_(true)
407 {
408 oldGlobalOffset_ = globalOffset_;
410 }
411
412 ~AdaptiveDofMapper () { DofManager< typename GridPartType::GridType >::instance( gridPart_.grid() ).removeIndexSet( *this ); }
413
414 AdaptiveDofMapper ( const AdaptiveDofMapper & ) = delete;
415 AdaptiveDofMapper ( AdaptiveDofMapper && ) = delete;
416
417 static constexpr bool consecutive () noexcept { return true; }
418
419 SizeType numBlocks () const { return numBlocks( std::index_sequence_for< Mapper ... >() ); }
420
421 SizeType numberOfHoles ( const int block ) const
422 {
423 SizeType nHoles = 0;
424 Hybrid::forEach( std::make_index_sequence< mapperTupleSize >{},
425 [ & ]( auto i )
426 {
427 const int localBlock = computeBlock( i, block );
428 if( localBlock >= 0 )
429 {
430 nHoles = std::get< i >( this->mapperTuple_ ).numberOfHoles( localBlock );
431 return;
432 }
433 } );
434 return nHoles;
435 }
436
437 GlobalKeyType oldIndex ( const int hole, const int block ) const
438 {
439 SizeType oIndex = 0;
440 Hybrid::forEach( std::make_index_sequence< mapperTupleSize >{},
441 [ & ]( auto i )
442 {
443 const int localBlock = computeBlock( i, block );
444 if( localBlock >= 0 )
445 {
446 oIndex = std::get< i >( this->mapperTuple_ ).oldIndex( hole, localBlock ) + globalOffset_[ i ];
447 return;
448 }
449 } );
450 return oIndex;
451 }
452
453 GlobalKeyType newIndex ( const int hole, const int block ) const
454 {
455 SizeType nIndex = 0;
456 Hybrid::forEach( std::make_index_sequence< mapperTupleSize >{},
457 [ & ]( auto i )
458 {
459 const int localBlock = computeBlock( i, block );
460 if( localBlock >= 0 )
461 {
462 nIndex = std::get< i >( this->mapperTuple_ ).newIndex( hole, localBlock ) + globalOffset_[ i ];
463 return ;
464 }
465 } );
466 return nIndex;
467 }
468
469 SizeType oldOffSet ( int block ) const
470 {
471 SizeType oOffset = 0;
472 Hybrid::forEach( std::make_index_sequence< mapperTupleSize >{},
473 [ & ]( auto i )
474 {
475 const int localBlock = computeBlock( i, block );
476 if( localBlock >= 0 )
477 {
478 oOffset = std::get< i >( this->mapperTuple_ ).oldOffSet( localBlock ) + oldGlobalOffset_[ i ];
479 return ;
480 }
481 } );
482 return oOffset;
483 }
484
485 SizeType offSet ( int block ) const
486 {
487 SizeType offset = 0;
488 Hybrid::forEach( std::make_index_sequence< mapperTupleSize >{},
489 [ & ]( auto i )
490 {
491 const int localBlock = computeBlock( i, block );
492 if( localBlock >= 0 )
493 {
494 offset = std::get< i >( this->mapperTuple_ ).offSet( localBlock ) + globalOffset_[ i ];
495 return ;
496 }
497 } );
498 return offset;
499 }
500
501 void resize () { update(); }
502
503 bool compress ()
504 {
505 update();
506 // after this step for both methods we need the full update
507 needsFullUpdate_ = true;
508 return true;
509 }
510
511 void backup () const {}
512
513 void restore () { compress(); }
514
515 template< class IOStream >
516 void read ( IOStream &in ) { compress(); }
517
518 template< class IOStream >
519 void write ( IOStream &out ) const {}
520
521 template< class Entity >
522 void insertEntity ( const Entity & )
523 {
524 if( needsFullUpdate_ )
525 {
526 // update also offsets in this case
527 update();
528 }
529 else
530 {
531 // call BaseType::update to avoid changing oldGlobalOffSet_
532 // do not use this->update here!
533 BaseType::update();
534 }
535 }
536
537 template< class Entity >
538 void removeEntity ( const Entity & ) {}
539
540 void update ()
541 {
542 // store previous offset
543 oldGlobalOffset_ = globalOffset_;
544
545 // in callback we always need the full update, otherwise set to false here
546 needsFullUpdate_ = isCallBackAdapt_ ;
547
548 // update component mappers and compute offsets
549 BaseType::update();
550 }
551
552 protected:
553 int computeBlock( const int i, const unsigned int block ) const
554 {
555 if( block >= blocks_[ i ] && block < blocks_[ i + 1 ] )
556 return block - blocks_[ i ];
557 else
558 return -1;
559 }
560
561 void printOffSet( const OffsetType& offset, const std::string& name ) const
562 {
563 for( const auto& off : offset )
564 {
565 std::cout << name << " = " << off << std::endl;
566 }
567 }
568
569 template< std::size_t ... i >
570 SizeType numBlocks ( std::index_sequence< i ... > ) const
571 {
572 return Std::sum( std::get< i >( mapperTuple_ ).numBlocks() ... );
573 }
574
575 SizeType computeNumBlocks ()
576 {
577 // compute blocks (only needs to be done once)
578 // length of blocks_ is mapperTupleSize + 1
579 blocks_[ 0 ] = 0;
580 Hybrid::forEach( std::make_index_sequence< mapperTupleSize >{},
581 [ & ]( auto i ){ blocks_[ i + 1 ] = blocks_[ i ] + std::get< i >( mapperTuple_ ).numBlocks(); } );
582
583 // last block number determines overall block numbers
584 return blocks_.back();
585 }
586
587
588 OffsetType oldGlobalOffset_;
589 OffsetType blocks_;
590
591 const SizeType numBlocks_;
592 const bool isCallBackAdapt_;
593 bool needsFullUpdate_;
594 };
595
596
597
598 // Implementation
599 // --------------
600
601 template< class GridPart, class ... Mapper >
602 struct Implementation
603 {
604 typedef typename std::conditional<
605 Std::And( Capabilities::isAdaptiveDofMapper< Mapper >::v ... ),
606 AdaptiveDofMapper< Traits< GridPart, Mapper ... > >,
607 DofMapper< Traits< GridPart, Mapper ... > > >::type Type;
608 };
609
610
611 } // namespace __TupleMapper
612
613#endif // #ifndef DOXYGEN
614
615
616 // TupleMapper
617 // -----------
618
629 template< class GridPart, class ... Mapper >
631 : public __TupleMapper::template Implementation< GridPart, Mapper ... >::Type
632 {
633 typedef typename __TupleMapper::template Implementation< GridPart, Mapper ... >::Type BaseType;
634
635 public:
636 TupleMapper ( GridPart &gridPart, Mapper & ... mapper ) : BaseType( gridPart, mapper ... ) {}
637 TupleMapper ( GridPart &gridPart, Mapper && ... mapper ) : BaseType( gridPart, std::move( mapper ) ... ) {}
638 };
639
640 // Capabilities
641 // ------------
642
643 namespace Capabilities
644 {
645 template< class GridPart, class ... Mapper >
646 struct isAdaptiveDofMapper< TupleMapper< GridPart, Mapper ... > >
647 {
648 static const bool v = Std::And( isAdaptiveDofMapper< Mapper >::v ... );
649 };
650
651 template< class GridPart, class ... Mapper >
652 struct isConsecutiveIndexSet< __TupleMapper::AdaptiveDofMapper< __TupleMapper::Traits< GridPart, Mapper ... > > >
653 {
654 static const bool v = true;
655 };
656
657 } // namespace Capabilities
658
659 } // namespace Fem
660
661} // namespace Dune
662
663#if __GNUC__ >= 13
664// turn the warnings back on
665#pragma GCC diagnostic pop
666#endif // #if defined(__GNUC__)
667
668#endif // #ifndef DUNE_FEM_SPACE_COMBINEDSPACE_TUPLEMAPPER_HH
Extended interface for adaptive DoF mappers.
Definition: dofmapper.hh:219
Interface for calculating the size of a function space for a grid on a specified level....
Definition: dofmapper.hh:43
mapper allocating one DoF per subentity of a given codimension
Definition: tuplemapper.hh:632
Mapper interface.
Definition: mapper.hh:110
void removeIndexSet(const IndexSetType &iset)
removed index set from dof manager's list of index sets
Definition: dofmanager.hh:1331
static ThisType & instance(const GridType &grid)
obtain a reference to the DofManager for a given grid
Definition: dofmanager.hh:1251
void addIndexSet(const IndexSetType &iset)
add index set to dof manager's list of index sets
Definition: dofmanager.hh:1296
#define DUNE_THROW(E,...)
Definition: exceptions.hh:314
constexpr void forEach(Range &&range, F &&f)
Range based for loop.
Definition: hybridutilities.hh:257
Dune namespace.
Definition: alignedallocator.hh:13
constexpr std::integral_constant< std::size_t, sizeof...(II)> size(std::integer_sequence< T, II... >)
Return the size of the sequence.
Definition: integersequence.hh:75
constexpr std::bool_constant<((II==value)||...)> contains(std::integer_sequence< T, II... >, std::integral_constant< T, value >)
Checks whether or not a given sequence contains a value.
Definition: integersequence.hh:137
STL namespace.
Creative Commons License   |  Legal Statements / Impressum  |  Hosted by TU Dresden & Uni Heidelberg  |  generated with Hugo v0.111.3 (Apr 13, 22:42, 2025)