DUNE-ACFEM (unstable)

cseoptimization.hh
1#ifndef __DUNE_ACFEM_EXPRESSIONS_CSEOPTIMIZATION_HH__
2#define __DUNE_ACFEM_EXPRESSIONS_CSEOPTIMIZATION_HH__
3
4#include "../common/ostream.hh"
5#include "../mpl/subtuple.hh"
6#include "../mpl/compare.hh"
7#include "../mpl/typetupleutil.hh"
8#include "examineutil.hh"
9#include "extract.hh"
10#include "treeextract.hh"
11#include "optimizationbase.hh"
12#include "runtimeequal.hh"
13#include "subexpression.hh"
14
15namespace Dune {
16
17 namespace ACFem {
18
19 namespace Expressions {
20
21 using ACFem::operator==;
22
24 //
25 template<class Expr, template<class...> class IsCandidate, std::ptrdiff_t N = -1>
26 struct IsSubExpressionCandidate
27 {
28 //T is a MPL::TypeTuple<Expr, TreePos, Parent>.
29 template<class T, class Parent, class SFINAE = void>
30 struct Predicate
31 : IsCandidate<T, Parent, Expr>
32 {};
33
34 //Restrict to the given expression depth.
35 template<class T, class Parent>
36 struct Predicate<T, Parent,
37 std::enable_if_t<std::is_same<Parent, void>::value || (N != -1 && Depth<T>::value != (std::size_t)N)> >
38 : FalseType
39 {};
40
41 template<class T>
42 using Ignore = BoolConstant<IsSelfExpression<T>::value || (Depth<T>::value < N)>;
43 };
44
46 template<class T, class SExp, class SFINAE = void>
48 : FalseType
49 {};
50
54 template<class T, class SExp>
56 T,
57 SExp,
58 std::enable_if_t<(IsSubExpressionExpression<T>::value
59 && AreRuntimeEqual<Operand<1, T>, Operand<1, SExp> >::value
60 )> >
61 : TrueType
62 {};
63
67 template<class Traits, class DepthTuple>
69
71 template<class T, class SFINAE = void>
73 : FalseType
74 {};
75
77 template<class T>
78 struct IsCSECascade<T, std::enable_if_t<!IsDecay<T>::value> >
79 : IsCSECascade<std::decay_t<T> >
80 {};
81
83 template<class Traits, class DepthTuple>
84 struct IsCSECascade<CommonSubExpressionCascade<Traits, DepthTuple> >
85 : TrueType
86 {};
87
88 /**********************************************************************
89 *
90 * Loop over a hierarchy of subexpressions. Think about a nested
91 * tuple of tuples. The inner tuples conatain a unique set of
92 * sub-expression for a given expression depth.
93 *
94 * The expressions in the higher-depth tuples may contain
95 * referencews to sub-expressions at lower level.
96 *
97 * There are two tasks:
98 *
99 * + generate such a nested structure from a given expression
100 * template.
101 *
102 * + if the structure has been built then copy constructions
103 * needs adjustment of the contained references.
104 *
105 */
106
107 namespace {
108 // helper functions for class construction
109
110 template<template<class...> class P>
111 struct OrMatchTreePos
112 {
113 template<class T1, class T2, class SFINAE = void>
114 struct Predicate;
115
116 template<class E, class TreePos, class SExp>
117 struct Predicate<
118 MPL::TypeTuple<E, TreePos>, SExp, std::enable_if_t<IsSubExpressionExpression<SExp>::value> >
119 : BoolConstant<(P<E, SExp>::value || std::is_same<TreePos, typename SubExpressionTraits<SExp>::TreePos>::value)>
120 {};
121 };
122
123 // Endpoint for one level.
124 template<template<class...> class MatchPredicate, class SExp, class TreePos, class PrevDepth>
125 constexpr decltype(auto) injectSubExpressionsOneLevel(
126 SExp&& sExp, TreePos, PrevDepth&& prevDepth, IndexSequence<>)
127 {
128 return forwardReturnValue<SExp>(sExp);
129 }
130
131 // Recursion at one level.
132 template<template<class...> class MatchPredicate, class SExp, class TreePos, class Traits, class PrevDepth, std::size_t I0, std::size_t... ArgIdx>
133 constexpr auto injectSubExpressionsOneLevel(
134 SExp&& sExp,
135 TreePos,
136 const CommonSubExpressionCascade<Traits, PrevDepth>& prevDepth,
137 IndexSequence<I0, ArgIdx...>)
138 {
139 return injectSubExpressionsOneLevel<MatchPredicate>(
140 replaceMatching<OrMatchTreePos<MatchPredicate>::template Predicate, IsSubExpressionExpression>(
141 std::forward<SExp>(sExp), std::get<I0>(prevDepth.get()), DontOptimize{}, TreePos{}),
142 TreePos{},
143 prevDepth,
144 IndexSequence<ArgIdx...>{});
145 }
146
147 // Recursion endpoint at lowest level.
148 template<template<class...> class MatchPredicate, class SExp, class Traits, class TreePos, std::enable_if_t<IsExpression<SExp>::value, int> = 0>
149 constexpr decltype(auto) injectSubExpressionsRecursion(
150 SExp&& sExp,
151 TreePos,
152 const CommonSubExpressionCascade<Traits, std::tuple<std::tuple<> > >& prevDepth)
153 {
154 return forwardReturnValue<SExp>(sExp);
155 }
156
157 // Recursion to previous level.
158 template<template<class...> class MatchPredicate, class SExp, class TreePos, class Traits, class PrevDepth, class... PrevDepthRest,
159 std::enable_if_t<(IsExpression<SExp>::value && sizeof...(PrevDepthRest) > 0), int> = 0>
160 constexpr decltype(auto) injectSubExpressionsRecursion(
161 SExp&& sExp,
162 TreePos,
163 const CommonSubExpressionCascade<Traits, std::tuple<PrevDepth, PrevDepthRest...> >& prevDepth)
164 {
165 using PrevProvider = CommonSubExpressionCascade<Traits, std::tuple<PrevDepthRest...> >;
166
167 // First replace this level subexpressions into sExp.
168 auto thisLevel = injectSubExpressionsOneLevel<MatchPredicate>(std::forward<SExp>(sExp),
169 TreePos{},
170 prevDepth,
171 MakeSequenceFor<PrevDepth>{});
172 // Then recurse to lower levels
173 return injectSubExpressionsRecursion<MatchPredicate>(asExpression(std::move(thisLevel)), TreePos{}, static_cast<const PrevProvider&>(prevDepth));
174 }
175
176 // Expander, replacing matching expressions by the provided
177 // sub-expressions.
178 template<class Tuple, class Traits, class PrevDepth, std::size_t... Idx>
179 constexpr auto injectSubExpressionsExpander(
180 Tuple&& tuple,
181 const CommonSubExpressionCascade<Traits, PrevDepth>& prevDepth,
182 IndexSequence<Idx...>)
183 {
184 return std::make_tuple(
186 subExpression<Traits::template Storage>(
187 injectSubExpressionsRecursion<AreRuntimeEqual>(
188 std::move(std::get<Idx>(std::forward<Tuple>(tuple)).first),
189 std::get<Idx>(std::forward<Tuple>(tuple)).second, // TreePosition Id
190 prevDepth
191 ),
192 std::get<Idx>(std::forward<Tuple>(tuple)).second
193 )
194 )...
195 );
196 }
197
198 // Expander replacing all existing sub-expressions with the
199 // provided ones.
200 template<class Tuple, class Traits, class PrevDepth, std::size_t... Idx>
201 constexpr auto replaceSubExpressionsExpander(
202 Tuple&& tuple,
203 const CommonSubExpressionCascade<Traits, PrevDepth>& prevDepth,
204 IndexSequence<Idx...>)
205 {
206 return std::make_tuple(
208 subExpression<Traits::template Storage>(
209 injectSubExpressionsRecursion<IsMatchingSubExpression>(
210 std::move(std::get<Idx>(std::forward<Tuple>(tuple)).operand(1_c)),
211 typename SubExpressionTraits<TupleElement<Idx, Tuple> >::TreePos{},
212 prevDepth
213 ),
214 typename SubExpressionTraits<TupleElement<Idx, Tuple> >::TreePos{}
215 )
216 )...
217 );
218 }
219 } // NS anonymous
220
232 template<class T, class... SExpNode, class Traits, class PrevDepth>
233 constexpr auto injectSubExpressions(T&& t, MPL::TypeTuple<SExpNode...>, const CommonSubExpressionCascade<Traits, PrevDepth>& prevDepth)
234 {
235 return std::make_tuple(
237 subExpression<Traits::template Storage>(
238 injectSubExpressionsRecursion<AreRuntimeEqual>(
239 std::move(treeOperand(std::forward<T>(t), typename SExpNode::Second{})),
240 typename SExpNode::Second{},
241 prevDepth
242 ),
243 typename SExpNode::Second{}
244 )
245 )...
246 );
247 }
248
262 template<class Expr, class Traits, class Cascade, class TreePos = IndexSequence<>,
263 std::enable_if_t<!IsTupleLike<Expr>::value, int> = 0>
264 constexpr auto injectSubExpressions(Expr&& expr, const CommonSubExpressionCascade<Traits, Cascade>& cascade, TreePos = TreePos{})
265 {
266 return expressionClosure(injectSubExpressionsRecursion<AreRuntimeEqual>(
267 asExpression(std::forward<Expr>(expr)),
268 TreePos{},
269 cascade));
270 }
271
285 template<class T, class Traits, class PrevDepth, std::enable_if_t<IsTupleLike<T>::value, int> = 0>
286 constexpr auto replaceSubExpressions(T&& tuple,
287 const CommonSubExpressionCascade<Traits, PrevDepth>& prevDepth)
288 {
289 return replaceSubExpressionsExpander(std::forward<T>(tuple), prevDepth, MakeSequenceFor<T>{});
290 }
291
307 template<class Expr, class Traits, class Cascade, class TreePos = IndexSequence<>,
308 std::enable_if_t<!IsTupleLike<Expr>::value, int> = 0>
309 constexpr auto replaceSubExpressions(Expr&& expr, const CommonSubExpressionCascade<Traits, Cascade>& cascade, TreePos = TreePos{})
310 {
311 return expressionClosure(injectSubExpressionsRecursion<IsMatchingSubExpression>(
312 asExpression(std::forward<Expr>(expr)),
313 TreePos{},
314 cascade));
315 }
316
317 /*
318 *
319 **********************************************************************/
320
322 template<class Traits>
323 class CommonSubExpressionCascade<Traits, std::tuple<std::tuple<> > >
324 {
325 static constexpr std::size_t depth_ = 0;
326 public:
327 using DepthType = std::tuple<>;
328 using DepthTupleType = std::tuple<DepthType>;
329 using TraitsType = Traits;
330
331 CommonSubExpressionCascade(Traits&& t = Traits{})
332 : traits_(std::forward<Traits>(t))
333 {}
334
335 CommonSubExpressionCascade(const std::tuple<>, Traits&& t = Traits{})
336 : traits_(std::forward<Traits>(t))
337 {}
338
339 template<std::size_t D = 0>
340 const DepthType& get(IndexConstant<D> = IndexConstant<D>{}) const&
341 {
342 static_assert(D == 0, "This is the terminal cascade element, D should be 0.");
343 return data_;
344 }
345
346 template<std::size_t D = 0>
347 DepthType& get(IndexConstant<D> = IndexConstant<D>{})&
348 {
349 static_assert(D == 0, "This is the terminal cascade element, D should be 0.");
350 return data_;
351 }
352
353 template<std::size_t D = 0>
354 DepthType get(IndexConstant<D> = IndexConstant<D>{})&&
355 {
356 static_assert(D == 0, "This is the terminal cascade element, D should be 0.");
357 return data_;
358 }
359
360 template<class... T>
361 void evaluate(T&&...) {} // we don't evaluate :)
362
363 constexpr static std::size_t depth()
364 {
365 return depth_;
366 }
367
368 static constexpr bool isEmpty()
369 {
370 return true;
371 }
372
373 const TraitsType& sExpTraits() const
374 {
375 return traits_;
376 }
377
378 protected:
379 std::tuple<> data_; // dummy
380 TraitsType traits_;
381 };
382
384 template<class Traits, class... SExp, class PrevSExp, class... SExpRest>
385 class CommonSubExpressionCascade<Traits, std::tuple<std::tuple<SExp...>, PrevSExp, SExpRest...> >
386 : public CommonSubExpressionCascade<Traits, std::tuple<PrevSExp, SExpRest...> >
387 {
388 using BaseType = CommonSubExpressionCascade<Traits, std::tuple<PrevSExp, SExpRest...> >;
389
391 ==
393 "Only allowed for proper sub-expressions.");
394 public:
395 using DepthType = std::tuple<SExp...>;
396 using DepthTupleType = std::tuple<std::tuple<SExp...>, PrevSExp, SExpRest...>;
397 using typename BaseType::TraitsType;
398 using BaseType::sExpTraits;
399
400 static_assert(std::is_same<typename BaseType::DepthType, PrevSExp>::value,
401 "Bug, inconsistent data types.");
402
403 static constexpr std::size_t depth_ = sizeof...(SExpRest) + 2;
404
405 template<class T, class... SExpNode>
406 CommonSubExpressionCascade(const BaseType& base, T&& t, MPL::TypeTuple<SExpNode...> sExpTuple)
407 : BaseType(base)
408 , data_(injectSubExpressions(std::forward<T>(t), sExpTuple, static_cast<const BaseType&>(*this)))
409 {
410 static_assert(sizeof...(SExpNode) == sizeof...(SExp), "Mismatching numbers of expressions.");
411 }
412
414 : BaseType(other)
415 , data_(replaceSubExpressions(const_cast<DepthType&>(other.data_), static_cast<const BaseType&>(*this)))
416 {}
417
419 : BaseType(other)
420 , data_(replaceSubExpressions(std::move(other.data_), static_cast<const BaseType&>(*this)))
421 {}
422
423 template<std::size_t D = 0, std::enable_if_t<(D == 0), int> = 0>
424 const DepthType& get(IndexConstant<D> = IndexConstant<D>{}) const&
425 {
426 return data_;
427 }
428
429 template<std::size_t D = 0, std::enable_if_t<(D == 0), int> = 0>
430 DepthType& get(IndexConstant<D> = IndexConstant<D>{}) &
431 {
432 return data_;
433 }
434
435 template<std::size_t D = 0, std::enable_if_t<(D == 0), int> = 0>
436 DepthType get(IndexConstant<D> = IndexConstant<D>{}) &&
437 {
438 return data_;
439 }
440
441 template<std::size_t D, std::enable_if_t<(D > 0), int> = 0>
442 const auto& get(IndexConstant<D> = IndexConstant<D>{}) const&
443 {
444 return BaseType::template get<D-1>();
445 }
446
447 template<std::size_t D, std::enable_if_t<(D > 0), int> = 0>
449 {
450 return BaseType::template get<D-1>();
451 }
452
453 template<std::size_t D, std::enable_if_t<(D > 0), int> = 0>
454 typename BaseType::DepthType get(IndexConstant<D> = IndexConstant<D>{}) &&
455 {
456 return BaseType::template get<D-1>();
457 }
458
460 template<class SExpTraits = TraitsType>
461 void evaluate(SExpTraits&& traits)
462 {
463 BaseType::evaluate(std::forward<SExpTraits>(traits)); // descend first
464 forEach(data_, [&traits](auto&& sExp) { // then evaluate this level
465 evaluateSubExpression(sExp, std::forward<SExpTraits>(traits));
466 });
467 }
468
469 void evaluate()
470 {
471 evaluate(sExpTraits());
472 }
473
474 auto base()&&
475 {
476 return static_cast<BaseType&&>(*this);
477 }
478
479 auto& base()&
480 {
481 return static_cast<BaseType&>(*this);
482 }
483
484 const auto& base() const&
485 {
486 return static_cast<const BaseType&>(*this);
487 }
488
489 constexpr static std::size_t depth()
490 {
491 return depth_;
492 }
493
494 static constexpr bool isEmpty()
495 {
496 return size<DepthType>() == 0 && BaseType::isEmpt();
497 }
498
499 protected:
500 DepthType data_;
501 };
502
503 template<class A, class B>
504 using Unique = Expressions::AreRuntimeEqual<typename A::First, typename B::First>;
505
506 template<class E, class TreePos, class... T>
507 using ProcessUnique = MPL::JoinUnless<Unique, T...>;
508
509 template<class T, class Traits, std::size_t D>
510 struct CascadeBuildTraits;
511
512 template<class T, class Traits>
513 struct CascadeBuildTraits<T, Traits, 0>
514 {
515 using ExprType = EnclosedType<T>;
516 using DepthType = MPL::TypeTuple<>;
517 using SExpTuple = std::tuple<>;
518 using DepthTuple = std::tuple<std::tuple<> >; // end of recursion.
519 using CascadeType = CommonSubExpressionCascade<Traits, DepthTuple>;
520 };
521
522 template<class T, class Traits, std::size_t N>
523 struct CascadeBuildTraits
524 {
525 using IsCandidate = IsSubExpressionCandidate<T, Traits::template IsCandidate, N>;
526 using ExprType = EnclosedType<T>;
527 using DepthType = Expressions::TreeExtract<
528 IsCandidate::template Predicate,
529 ExprType, Expressions::PackTreePair, Expressions::TreePosition<>,
530 IsCandidate::template Ignore, ProcessUnique>;
531 using PrevTraits = CascadeBuildTraits<T, Traits, N-1>;
532 using PrevCascade = typename PrevTraits::CascadeType;
533 using SExpTuple =
534 std::decay_t<decltype(injectSubExpressions(std::declval<T>(), DepthType{}, std::declval<const PrevCascade&>()))>;
535 static constexpr bool empty = size<SExpTuple>() == 0;
536 using DepthTuple = ConditionalType<empty,
537 typename PrevTraits::DepthTuple,
538 AddFrontTuple<SExpTuple, typename PrevTraits::DepthTuple> >;
539 using CascadeType = CommonSubExpressionCascade<Traits, DepthTuple>;
540 };
541
542 template<class T, class Traits, std::size_t N = Depth<T>::value,
543 std::enable_if_t<(IsExpression<T>::value
544 && !IsClosure<T>::value
545 && N == 0
546 ), int> = 0>
547 auto commonSubExpressionCascade(T&& t, Traits&& traits, IndexConstant<N> = IndexConstant<N>{})
548 {
549 //std::clog << __PRETTY_FUNCTION__ << std::endl;
550 using BuildTraits = CascadeBuildTraits<T, Traits, N>;
551 using CascadeType = typename BuildTraits::CascadeType;
552 return CascadeType(std::tuple<>{}, std::forward<Traits>(traits));
553 }
554
566 template<class T, class Traits, std::size_t N = Depth<T>::value,
567 std::enable_if_t<(IsExpression<T>::value
568 && !IsClosure<T>::value
569 && N > 0
570 ), int> = 0>
571 auto commonSubExpressionCascade(T&& t, Traits&& traits, IndexConstant<N> = IndexConstant<N>{})
572 {
573 //std::clog << __PRETTY_FUNCTION__ << std::endl;
574 using BuildTraits = CascadeBuildTraits<T, Traits, N>;
575 if constexpr (BuildTraits::empty) {
576 return Expressions::commonSubExpressionCascade(std::forward<T>(t), std::forward<Traits>(traits), IndexConstant<N-1>{});
577 } else {
578 using IsCandidate = typename BuildTraits::IsCandidate;
579 using SExpTuple = Expressions::TreeExtract<
580 IsCandidate::template Predicate,
581 T, Expressions::PackTreePair, Expressions::TreePosition<>,
582 IsCandidate::template Ignore, ProcessUnique>;
583
584 using CascadeType = typename BuildTraits::CascadeType;
585 return CascadeType(
586 Expressions::commonSubExpressionCascade(std::forward<T>(t), std::forward<Traits>(traits), IndexConstant<N-1>{}),
587 std::forward<T>(t), SExpTuple{}
588 );
589 }
590 }
591
592 template<class T, class Traits,
593 std::enable_if_t<(IsExpression<T>::value
594 && IsClosure<T>::value
595 ), int> = 0>
596 auto commonSubExpressionCascade(T&& t, Traits&& traits)
597 {
598 return Expressions::commonSubExpressionCascade(asExpression(std::forward<T>(t)), std::forward<Traits>(traits));
599 }
600
601 namespace {
602
603 template<template<class T, class...> class F,
604 std::size_t N,
605 class Input,
606 class Initial,
607 class Recursive,
608 template<class...> class Unique,
609 std::enable_if_t<(N >= size<Input>()), int> = 0>
610 auto extractFromCSEHelper(Input&& input,
611 Initial&& initial,
612 Recursive r, PredicateWrapper<Unique> u)
613 {
614 return std::forward<Initial>(initial);
615 }
616
617 template<template<class T, class...> class F,
618 std::size_t N,
619 class Input,
620 class Initial,
621 class Recursive,
622 template<class...> class Unique,
623 std::enable_if_t<(N < size<Input>()), int> = 0>
624 auto extractFromCSEHelper(Input&& input,
625 Initial&& initial,
626 Recursive r, PredicateWrapper<Unique> u)
627 {
628 return extractFromCSEHelper<F, N+1>(
629 std::forward<Input>(input),
630 extract<F>(
631 //std::forward<TupleElement<N, Input> >(get<N>(std::forward<Input>(input))),
632 get<N>(std::forward<Input>(input)),
633 std::forward<Initial>(initial),
634 r, u
635 ),
636 r, u
637 );
638 }
639 }
640
650 template<template<class T, class...> class F,
651 class T,
652 class Initial = std::tuple<>,
653 class Recursive = FalseType,
654 template<class...> class Unique = AlwaysFalse,
655 std::enable_if_t<IsCSECascade<T>::value && std::decay_t<T>::depth() != 0, int> = 0>
656 auto extract(T&& cascade,
657 Initial&& initial = Initial{},
658 Recursive r = Recursive{},
659 PredicateWrapper<Unique> u = PredicateWrapper<Unique>{})
660 {
661 return extract<F>(std::forward<T>(cascade).base(),
662 extractFromCSEHelper<F, 0>(
663 std::forward<T>(cascade).get(),
664 std::forward<Initial>(initial),
665 r, u
666 ),
667 r, u);
668 }
669
671 template<template<class T, class...> class F,
672 class T,
673 class Initial = std::tuple<>,
674 class Recursive = FalseType,
675 template<class...> class Unique = AlwaysFalse,
676 std::enable_if_t<IsCSECascade<T>::value && std::decay_t<T>::depth() == 0, int> = 0>
677 auto extract(T&& cascade,
678 Initial&& initial = Initial{},
679 Recursive r = Recursive{},
680 PredicateWrapper<Unique> u = PredicateWrapper<Unique>{})
681 {
682 return std::forward<Initial>(initial);
683 }
684
685 } // NS Expressions
686
687 } // ACFem
688
689} // Dune
690
691#endif // __DUNE_ACFEM_EXPRESSIONS_CSEOPTIMIZATION_HH__
void evaluate(SExpTraits &&traits)
Evalute all contained sub-expressions, lowest level first.
Definition: cseoptimization.hh:461
Wrap tuple of subexpressions with constant references to sub-expressions of lower levels.
Definition: cseoptimization.hh:68
constexpr decltype(auto) asExpression(T &&t)
Return a non-closure expression as is.
Definition: interface.hh:122
constexpr decltype(auto) expressionClosure(T &&t)
Do-nothing default implementation for pathologic cases.
Definition: interface.hh:93
OptimizeTag< 0 > DontOptimize
Bottom level is overloaded to do nothing.
Definition: optimizationbase.hh:74
constexpr decltype(auto) evaluate(Optimize, T &&t)
Evaluate an expression or expression Expressions::Storage container.
Definition: optimizationbase.hh:206
constexpr decltype(auto) get(T &&t, IndexConstant< I >=IndexConstant< I >{})
Access to the i-the element.
Definition: access.hh:129
void evaluateSubExpression(T &&t, Functor &&f=Functor{})
Copy the value of the contained sub-expression (operand no.
Definition: subexpression.hh:125
Sequence< std::size_t, V... > IndexSequence
Sequence of std::size_t values.
Definition: types.hh:64
Constant< bool, V > BoolConstant
Short-cut for integral constant of type bool.
Definition: types.hh:48
Constant< std::size_t, V > IndexConstant
Short-cut for integral constant of type std::size_t.
Definition: types.hh:44
typename MakeType< FalseType, Other... >::Type AlwaysFalse
Generate FalseType regardless of the template argument list.
Definition: types.hh:155
BoolConstant< false > FalseType
Alias for std::false_type.
Definition: types.hh:110
BoolConstant< true > TrueType
Alias for std::true_type.
Definition: types.hh:107
STL namespace.
Definition: cseoptimization.hh:74
Definition: cseoptimization.hh:49
Creative Commons License   |  Legal Statements / Impressum  |  Hosted by TU Dresden  |  generated with Hugo v0.111.3 (Jul 15, 22:36, 2024)