diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/main/.buildinfo b/main/.buildinfo new file mode 100644 index 0000000..03a5117 --- /dev/null +++ b/main/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 500d46da7609ef2f82a8c46efb8152dd +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/main/Benchmarking/DiffusionEquations.html b/main/Benchmarking/DiffusionEquations.html new file mode 100644 index 0000000..41ec00d --- /dev/null +++ b/main/Benchmarking/DiffusionEquations.html @@ -0,0 +1,538 @@ + + + + + + + + + + + <no title> — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ + + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Benchmarking/NavierStokes.html b/main/Benchmarking/NavierStokes.html new file mode 100644 index 0000000..527cef5 --- /dev/null +++ b/main/Benchmarking/NavierStokes.html @@ -0,0 +1,553 @@ + + + + + + + + + + + Navier-Stokes Benchmarks — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Navier-Stokes Benchmarks

+ +
+ +
+
+ + + + +
+ + + + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Benchmarking/Stokes.html b/main/Benchmarking/Stokes.html new file mode 100644 index 0000000..91f17f5 --- /dev/null +++ b/main/Benchmarking/Stokes.html @@ -0,0 +1,559 @@ + + + + + + + + + + + Stokes Flow — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Stokes Flow

+ +
+ +
+
+ + + + +
+ +
+

Stokes Flow#

+
+
+

Analytic benchmarking#

+
+
+

Symmetry testing#

+

(e.g. mode coupling etc where we know which modes should be excluded)

+
+
+

Free surface models#

+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Bibliography.html b/main/Bibliography.html new file mode 100644 index 0000000..5206a32 --- /dev/null +++ b/main/Bibliography.html @@ -0,0 +1,547 @@ + + + + + + + + + + + Bibliography — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Bibliography

+ +
+
+ +
+
+
+ + + + +
+ +
+

Bibliography#

+ +
+
+
+[KBRS13] +

Matthew G. Knepley, Jed Brown, Karl Rupp, and Barry F. Smith. Achieving High Performance with Unified Residual Evaluation. arXiv:1309.1204 [cs], September 2013. arXiv:1309.1204.

+
+
+[MQL+07] +

L. Moresi, S. Quenette, V. Lemiale, C. Mériaux, B. Appelbe, and H. -B. Mühlhaus. Computational approaches to studying non-linear dynamics of the crust and mantle. Physics of the Earth and Planetary Interiors, 163(1):69–82, August 2007. doi:10.1016/j.pepi.2007.06.009.

+
+
+
+
+
+
+
+
+
+

.

+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/CiteEveryReference.html b/main/CiteEveryReference.html new file mode 100644 index 0000000..1074278 --- /dev/null +++ b/main/CiteEveryReference.html @@ -0,0 +1,545 @@ + + + + + + + + + + + <no title> — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ + +

[MQL+07]

+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Exercises/Figures/README.html b/main/Exercises/Figures/README.html new file mode 100644 index 0000000..2405f18 --- /dev/null +++ b/main/Exercises/Figures/README.html @@ -0,0 +1,539 @@ + + + + + + + + + + + <no title> — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +

Figures associated with “Exercises” for the jupyterbook

+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Exercises/README.html b/main/Exercises/README.html new file mode 100644 index 0000000..563e427 --- /dev/null +++ b/main/Exercises/README.html @@ -0,0 +1,528 @@ + + + + + + + + + + + Exercises — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Exercises

+ +
+
+ +
+
+
+ + + + +
+ +
+

Exercises#

+

Add questions to include in the JB … don’t add answers because a private repo +cannot be clones to jupyterhub with nbgitpuller.

+

The use of general “admonitions” is quite a good format for individual questions and it is useful to separate them out into a separate markdown file if you want to create a printable pdf.

+
+

Q1 - How to create a question

+

Suppose you want to create a series of textbook style questions for your jupyterbook. +The goal might be just to have a bunch of questions to choose from or it may be that you would +like to release a worksheet.

+

Try using a general jupyterbook admonition to create a question card and see if that works for you.

+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Figures/Diagrams/README.html b/main/Figures/Diagrams/README.html new file mode 100644 index 0000000..7d0b83e --- /dev/null +++ b/main/Figures/Diagrams/README.html @@ -0,0 +1,552 @@ + + + + + + + + + + + Diagrams / Figures — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Diagrams / Figures

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

Diagrams / Figures#

+ +

Note: for sketches / figures, please convert files to inkscape-compatible format at some point

+
+

Figure captions / descriptions.#

+
    +
  • NS_Benchmark_DFG_2.png: this is the benchmark for Reynolds number 100 in which the flow is a periodic shedding of vortices. +This figure shows the flow and flow markers two periods apart in a low resolution calculation. In the upper image of this figure, the vortex that is just being +shed from the top side of the cylinder is just about to be swept out of the domain in the lower image. UW is clearly capturing the +periodicity well but the benchmark also demands accurate computation of lift, drag, pressure, frequency etc.

  • +
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Figures/Movies/NS_Benchmark_DFG_2.small.m4v b/main/Figures/Movies/NS_Benchmark_DFG_2.small.m4v new file mode 100644 index 0000000..8bee1be Binary files /dev/null and b/main/Figures/Movies/NS_Benchmark_DFG_2.small.m4v differ diff --git a/main/Figures/Movies/etopoimageH.mp4 b/main/Figures/Movies/etopoimageH.mp4 new file mode 100644 index 0000000..fee8d4b Binary files /dev/null and b/main/Figures/Movies/etopoimageH.mp4 differ diff --git a/main/Figures/PDFs/README.html b/main/Figures/PDFs/README.html new file mode 100644 index 0000000..7669be2 --- /dev/null +++ b/main/Figures/PDFs/README.html @@ -0,0 +1,539 @@ + + + + + + + + + + + <no title> — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +

This folder is for PDF files that can be embedded in the book (as a presentation, for example).

+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Figures/PDFs/README.md b/main/Figures/PDFs/README.md new file mode 100644 index 0000000..ad99c63 --- /dev/null +++ b/main/Figures/PDFs/README.md @@ -0,0 +1 @@ +This folder is for PDF files that can be embedded in the book (as a presentation, for example). \ No newline at end of file diff --git a/main/FrontPage.html b/main/FrontPage.html new file mode 100644 index 0000000..c111122 --- /dev/null +++ b/main/FrontPage.html @@ -0,0 +1,622 @@ + + + + + + + + + + + The Underworld Geodynamics Platform — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + +
+ +
+

The Underworld Geodynamics Platform#

+
+

Warning

+

Warning - Underworld 3 is still in \(\beta\) release

+
+

+
+

A parallel, python, particle-in-cell, finite-element code for Geodynamics#

+

Underworld is a python-friendly geodynamics code which provides a programmable and flexible front end to all the functionality of the code running in a parallel HPC environment. This gives significant advantages to the user, with access to the power of python libraries for setup of complex problems, analysis at runtime, problem steering, and coupling of multiple problems. Underworld is integrated with the literate programming environment of the jupyter notebook system for tutorials and as a teaching tool for solid Earth geoscience.

+

Underworld is an open-source, particle-in-cell finite element code tuned for large-scale geodynamics simulations. The numerical algorithms allow the tracking of history information through the high-strain deformation associated with fluid flow (for example, transport of the stress tensor in a viscoelastic, convecting medium, or the advection of fine-scale damage parameters by the large-scale flow). The finite element mesh can be static or dynamic, but it is not constrained to move in lock-step with the evolving geometry of the fluid. This hybrid approach is very well suited to complex fluids which is how the solid Earth behaves on a geological timescale.

+ +
+

Governance#

+

Underworld is funded by AuScope which is part of the Australian Government’s NCRIS initiative to provide community research infrastructure (please see www.auscope.org.au for more information).

+

The Underworld development team is based in Australia at the Australian National University, the University of Sydney and at Monash University and is led by Louis Moresi (ANU).

+

All development is overseen by a steering committee drawn from the supporting organisations and representatives from the Underworld community.

+
+
+

Background#

+

The numerical methods have been published in detail in Moresi et al, (2002, 2003). These papers dealt primarily with 2D applications but in recent years, we have introduced a number of improvements in the method to enable us to scale the problem to 3D (Moresi et al, 2007). For example we use a fast discrete Voronoi method to compute the integration weights of the particle-to-mesh mapping efficiently (Velic et al, 2009). We have also concentrated on extremely robust solvers / preconditioners which are necessary because the material variations and geometrical complexity are both large and unpredictable when we start of the simulation.

+

The benefit of this approach is associated with the separation of the computational mesh from the swarm of points which track the history. This allows us to retain a much more structured computational mesh than the deformation / material history would otherwise allow. We can take full advantage of the most efficient geometrical multigrid solvers and there is no need to preserve structure during any remeshing operations we undertake (for example if we do need to track a free surface or an internal interface). Although there are several complexities introduced by enforcing this separation, we find that the benefits, for our particular class of problems, are significant.

+
+
+

Implementation and parallelism#

+

The numerical solvers are based around the PETSc software suite which focuses on delivering good parallel scalability (up to thousands-of-cores). Our experience to date shows good scalability 2000+ cores

+
+
+
+

Acknowledgements#

+

We would like to acknowledge AuScope Simulation, Analysis and Modelling for providing long term funding which has made the project possible. Additional funding for specific improvements and additional functionality has come from the Australian Research Council (http://www.arc.gov.au). The python toolkit was funded by the NeCTAR eresearch_tools program. Underworld2 was originally developed in collaboration with the Victorian Partnership for Advanced Computing.

+

The documentation and tutorial materials provided by the authors are open source under a creative commons licence. +We acknowledge the contribution of the community in providing other materials and we endeavour to provide the correct attribution and citation. Please contact louis.moresi@anu.edu.au for updates and corrections.

+
+
+
+

Accessibility#

+

  

+

The html can also be typeset using the Atkinson Hyperlegible font everywhere, other than monospaced computer code, as an aid to legibility. This button is also located at the bottom of the left navigation bar on every page and will toggle between settings.

+
+
+
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Glossary.html b/main/Glossary.html new file mode 100644 index 0000000..a44ecb9 --- /dev/null +++ b/main/Glossary.html @@ -0,0 +1,527 @@ + + + + + + + + + + + Glossary — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Glossary

+ +
+
+ +
+
+
+ + + + +
+ +
+

Glossary#

+

Below are some important definitions.

+
+
Cloud Computing#

the on-demand availability of computer system resources, especially data storage (cloud storage) and computing power, without direct active management by the user.[2] Large clouds often have functions distributed over multiple locations, each location being a data center. Cloud computing relies on sharing of resources to achieve coherence and economies of scale (Wikipedia)

+
+
FEM#

The Finite Element Method

+
+
JupyterHub #

brings the power of notebooks to groups of users. It gives users access to computational environments and resources without burdening the users with installation and maintenance tasks. Users - including students, researchers, and data scientists - can get their work done in their own workspaces on shared resources which can be managed efficiently by system administrators.

+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Lectures/Index.html b/main/Lectures/Index.html new file mode 100644 index 0000000..80351e8 --- /dev/null +++ b/main/Lectures/Index.html @@ -0,0 +1,526 @@ + + + + + + + + + + + Lectures and Tutorials for Underworld 3 — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Lectures and Tutorials for Underworld 3

+ +
+
+ +
+
+
+ + + + +
+ +
+

Lectures and Tutorials for Underworld 3#

+

This is named “Lectures” because it comes from the course-notes template but here it really stands for tutorial material.

+

Two examples of lecture notes and simple slideshows that we can embed.

+

The lecture notes are in standard myst markdown format like the rest of the book.

+

The slideshows are built along with the book using reveal-md. They are supposed to +be very simple, versionable (text based, not binaries) and rely on external links for +complicated media such as large movies.

+

Lecture 1 is a demonstration

+

Lecture 2 has a bit of information about the reveal-md slides

+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Lectures/Lecture_example_1.html b/main/Lectures/Lecture_example_1.html new file mode 100644 index 0000000..a1ffd3c --- /dev/null +++ b/main/Lectures/Lecture_example_1.html @@ -0,0 +1,525 @@ + + + + + + + + + + + Lecture 1 - Notes — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Lecture 1 - Notes

+ +
+
+ +
+
+
+ + + + +
+ +
+

Lecture 1 - Notes#

+

A lecture can have some notes and a slideshow.

+ +

The embedding is via an html iframe that points to the built path (all the slides are rendered into the +slideshows directory at the root level of the book)

+
<iframe src="../slideshows/example_slide_deck1.reveal.html" title="Slideshow" width=100%, height=500, allowfullscreen></iframe>
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Lectures/Lecture_example_2.html b/main/Lectures/Lecture_example_2.html new file mode 100644 index 0000000..0d5a7f8 --- /dev/null +++ b/main/Lectures/Lecture_example_2.html @@ -0,0 +1,634 @@ + + + + + + + + + + + Lecture 2 - Slides — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Lecture 2 - Slides

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

Lecture 2 - Slides#

+

This is an embedded slide show that will show you a few tricks.

+ +

The embedding is via an html iframe that points to the built path (all the slides are rendered into the +slideshows directory at the root level of the book)

+
<iframe src="../slideshows/example_slide_deck2.reveal.html" title="Slideshow" width=100%, height=500, allowfullscreen></iframe>
+
+
+
+

Source#

+

This is the source code for the slide deck. It is mostly (reveal-md) markdown with 1) a yaml header that has reveal options in it, 2) html tags that use reveal.js styling. Note that it is relatively easy to make a few slides this way but more than this can become complicated.

+
separator: '<--o-->  '
+verticalSeparator: '<--v-->'
+revealOptions:
+#    transition: 'fade'
+
+    slideNumber: true
+    width:  1100
+    height: 750
+    margin: 0.07
+---
+
+# Slides
+
+- Louis Moresi
+- Australian National University
+
+<--o-->
+
+## Slide 2
+
+Typically, we have one or two images on a slide 
+
+<img class="r-stretch" data-src="images/LithosphereThickness.png">
+
+and text that explains what is going on. 
+The markdown image tags are limiting but `reveal.js` has image
+classes that can be used directly without too much bother:
+
+```html
+<img class="r-stretch" data-src="images/LithosphereThickness.png">
+```
+
+<--o-->
+
+## Slide 3
+
+Animations / styling work using `reveal.js` classes 
+
+<p class="fragment">Fade in</p>
+<p class="fragment fade-out">Fade out</p>
+<p class="fragment highlight-red">Highlight red</p>
+<p class="fragment fade-in-then-out">Fade in, then out</p>
+<p class="fragment fade-up">Slide up while fading in</p>
+
+<--o-->
+
+## Slide 4 Math
+
+Mathematics via *mathjax*
+
+$$ e^{i\pi} + 1 = 0$$
+
+With inline available ($e^{i\pi} = -1$) as well
+
+<--o-->
+
+## Slide 5 Vertical slides
+
+Reveal has vertical sub-stacks that you can divert through
+
+ - Vertical stack 1
+
+<--v-->
+
+## Slide 5.1 Vertical slides
+
+
+Reveal has vertical sub-stacks that you can divert through
+
+ - Vertical stack 2
+
+<--v-->
+
+## Slide 5.2 Vertical slides
+
+Reveal has vertical sub-stacks that you can divert through
+
+![Earth](images/LithosphereThickness.png) <!-- .element height="50%" width="50%" -->
+
+<--o-->
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Lectures/LecturesFolder.html b/main/Lectures/LecturesFolder.html new file mode 100644 index 0000000..a582ec3 --- /dev/null +++ b/main/Lectures/LecturesFolder.html @@ -0,0 +1,519 @@ + + + + + + + + + + + Lectures & Presentations — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Lectures & Presentations

+ +
+
+ +
+
+
+ + + + +
+ +
+

Lectures & Presentations#

+

reveal-md and embedded PDF are supported for lectures or tutorial presentations that are included in this book.

+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Lectures/movies/README.html b/main/Lectures/movies/README.html new file mode 100644 index 0000000..09d3cb1 --- /dev/null +++ b/main/Lectures/movies/README.html @@ -0,0 +1,539 @@ + + + + + + + + + + + <no title> — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +

I wonder what goes in this folder ?

+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Lectures/static_slides/slideshows/movies/README.html b/main/Lectures/static_slides/slideshows/movies/README.html new file mode 100644 index 0000000..f2a0bfb --- /dev/null +++ b/main/Lectures/static_slides/slideshows/movies/README.html @@ -0,0 +1,539 @@ + + + + + + + + + + + <no title> — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +

I wonder what goes in this folder ?

+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Manual/ConstitutiveModels.html b/main/Manual/ConstitutiveModels.html new file mode 100644 index 0000000..76e972a --- /dev/null +++ b/main/Manual/ConstitutiveModels.html @@ -0,0 +1,518 @@ + + + + + + + + + + + Constitutive Models — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Constitutive Models

+ +
+
+ +
+
+
+ + + + +
+ +
+

Constitutive Models#

+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Manual/Index.html b/main/Manual/Index.html new file mode 100644 index 0000000..395ca44 --- /dev/null +++ b/main/Manual/Index.html @@ -0,0 +1,559 @@ + + + + + + + + + + + Introduction to Underworld 3 — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Introduction to Underworld 3

+ +
+
+ +
+
+
+ + + + +
+ +
+

Introduction to Underworld 3#

+ +

An introduction to the components of an underworld model:

+ +
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Manual/Meshes.html b/main/Manual/Meshes.html new file mode 100644 index 0000000..5161bcb --- /dev/null +++ b/main/Manual/Meshes.html @@ -0,0 +1,537 @@ + + + + + + + + + + + Meshes and model objects — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Meshes and model objects

+ +
+
+ +
+
+
+ + + + +
+ +
+

Meshes and model objects#

+

The most fundamental object within Underworld is the Mesh object. This object describes the geometry, boundary conditions, coordinate system, holds discrete variables (structured on the mesh or unstructured swarms).

+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Manual/Solvers.html b/main/Manual/Solvers.html new file mode 100644 index 0000000..7aa87a5 --- /dev/null +++ b/main/Manual/Solvers.html @@ -0,0 +1,519 @@ + + + + + + + + + + + Solver Objects — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Solver Objects

+ +
+
+ +
+
+
+ + + + +
+ +
+

Solver Objects#

+

Templates for equations systems.

+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Manual/Swarms.html b/main/Manual/Swarms.html new file mode 100644 index 0000000..570cab6 --- /dev/null +++ b/main/Manual/Swarms.html @@ -0,0 +1,577 @@ + + + + + + + + + + + Particle Swarms — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Particle Swarms

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

Particle Swarms#

+
+

Swarm Variables#

+

Swarm variables are piecewise continuous functions that cannot be integrated directly +with the current pointwise-function requirements of PETScDS. Instead we map the +piecewise function to a continuous, mesh variable which we refer to as a proxy swarm +variable.

+
+\[ \int_\Omega \phi u_n = \int_\Omega \phi u_p \]
+

The left hand side of this equation can be assembled using the standard finite element +machinery of PETScDS, but, the right hand side must be integrated in a manner that accounts +for the piecewise nature of the swarm variable information.

+

One way in which we can build the right hand side is to use an inverse-distance weighted interpolation to an intermediary mesh variable (of comparable spatial density to the swarm, or higher polynomial degree). Alternatively, we can form a piecewise-pointwise function that matches the PETScDS template, although, strictly, this falls outside the restrictions imposed by that framework.

+

At first it can seem over-complicated to introduce another equation system that needs to be solved in order to use Lagrangian particle information interchangeably with mesh variables but there are some distinct advantages. In the traditional particle-in-cell or material-point finite element method, there is an ambiguity in the way in which we map Lagrangian variables to their mesh equivalents. We rely on an average over the path of a particle to cancel fluctuations that occur due to uneven particle distributions relative to integration points or nodal points. There is no consistent way for us to ensure that constraints are correctly applied, boundary conditions are satisfied or null-space modes are damped. However, these constraints can all be included in the projection equation to ensure that the proxy variables are compatible with the other mesh-variables used to build systems of equations.

+

In the code, we substitute proxy variables at the just-in-time compilation stage automatically.

+

NOTE: - currently we don’t really do that, but we probably should: swarm variable, boundary conditions, bespoke integration … do all that in each update phase.

+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Manual/Sympy.html b/main/Manual/Sympy.html new file mode 100644 index 0000000..3f44f71 --- /dev/null +++ b/main/Manual/Sympy.html @@ -0,0 +1,519 @@ + + + + + + + + + + + Sympy Interface in Underworld — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Sympy Interface in Underworld

+ +
+
+ +
+
+
+ + + + +
+ +
+

Sympy Interface in Underworld#

+

The interface between the underworld3 parallel data model and the user is through a combination of symbolic manupulation of data objects through mathematical expressions using sympyand access to raw data through a numpy

+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Manual/VariablesAndFunctions.html b/main/Manual/VariablesAndFunctions.html new file mode 100644 index 0000000..11a7840 --- /dev/null +++ b/main/Manual/VariablesAndFunctions.html @@ -0,0 +1,562 @@ + + + + + + + + + + + Underworld Variables — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Underworld Variables

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

Underworld Variables#

+
+

Mesh Variables#

+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/.virtual_documents/Examples-StokesFlow/Ex_Stokes_Cartesian_SolC.html b/main/Notebooks/.virtual_documents/Examples-StokesFlow/Ex_Stokes_Cartesian_SolC.html new file mode 100644 index 0000000..f4c00c3 --- /dev/null +++ b/main/Notebooks/.virtual_documents/Examples-StokesFlow/Ex_Stokes_Cartesian_SolC.html @@ -0,0 +1,906 @@ + + + + + + + + + + + <no title> — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+
+
import petsc4py
+from petsc4py import PETSc
+
+# options = PETSc.Options()
+# options["help"] = None
+
+import os
+
+os.environ["UW_TIMING_ENABLE"] = "1"
+
+
+import underworld3 as uw
+from underworld3.systems import Stokes
+from underworld3 import function
+from underworld3 import timing
+
+import numpy as np
+
+
+
+
+
+
+
n_els = 4
+refinement = 3
+
+mesh1 = uw.meshing.UnstructuredSimplexBox(
+    regular=True,
+    minCoords=(0.0, 0.0),
+    maxCoords=(1.0, 1.0),
+    cellSize=1 / n_els,
+    qdegree=3,
+    refinement=refinement,
+)
+
+mesh2 = uw.meshing.StructuredQuadBox(
+    elementRes=(n_els, n_els),
+    minCoords=(0.0, 0.0),
+    maxCoords=(1.0, 1.0),
+    qdegree=3,
+    refinement=refinement,
+)
+
+mesh = mesh1
+
+
+mesh.dm.view()
+
+
+
+
+
+
+
v = uw.discretisation.MeshVariable(
+    "v", mesh, mesh.dim, degree=2, varsymbol=r"\mathbf{u}"
+)
+p = uw.discretisation.MeshVariable(
+    "p", mesh, 1, degree=1, continuous=False, varsymbol=r"{p}"
+)
+T = uw.discretisation.MeshVariable(
+    "T", mesh, 1, degree=3, continuous=True, varsymbol=r"{T}"
+)
+T2 = uw.discretisation.MeshVariable(
+    "T2", mesh, 1, degree=3, continuous=True, varsymbol=r"{T_2}"
+)
+
+
+
+
+
+
+
stokes = uw.systems.Stokes(mesh, velocityField=v, pressureField=p)
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel(v)
+stokes.constitutive_model.Parameters.viscosity = 1
+
+
+
+
+
+
+
# Set some things
+import sympy
+from sympy import Piecewise
+
+
+x, y = mesh.X
+
+
+mesh.get_min_radius()
+
+
+hw = 0.1 * mesh.get_min_radius()
+surface_fn = 2 * uw.maths.delta_function(y - 1, hw) / uw.maths.delta_function(0.0, hw)
+base_fn = 2 * uw.maths.delta_function(y, hw)
+right_fn = 2 * uw.maths.delta_function(x - 1, hw)
+left_fn = 2 * uw.maths.delta_function(x, hw)
+
+
+surface_fn
+
+
+uw.function.evalf(surface_fn, np.array([[0.0, 1.0]]))
+
+
+# options = PETSc.Options()
+# options.getAll()
+
+
+eta_0 = 1.0
+x_c = 0.5
+f_0 = 1.0
+
+
+stokes.penalty = 100.0
+stokes.bodyforce = sympy.Matrix(
+    [
+        0,
+        Piecewise(
+            (f_0, x > x_c),
+            (0.0, True),
+        ),
+    ]
+)
+
+
+# This is the other way to impose no vertical
+
+stokes.bodyforce[0] -= 1.0e6 * v.sym[0] * (left_fn + right_fn)
+stokes.bodyforce[1] -= 1.0e3 * v.sym[1] * (surface_fn + base_fn)
+
+# stokes.add_natural_bc( -1.0e10 * v.sym[1], sympy.Matrix((0.0, 0.0)).T , "Top", components=[1])
+
+
+# free slip.
+# note with petsc we always need to provide a vector of correct cardinality.
+
+stokes.add_dirichlet_bc((sympy.oo, 0.0), "Bottom")
+stokes.add_dirichlet_bc((sympy.oo, 0.0), "Top")
+stokes.add_dirichlet_bc((0.0, sympy.oo), "Left")
+stokes.add_dirichlet_bc((0.0, sympy.oo), "Right")
+
+
+# stokes.petsc_options["snes_rtol"] = 1.0e-6
+# stokes.petsc_options["ksp_rtol"] = 1.0e-6
+# stokes.petsc_options["snes_max_it"] = 10
+
+
+stokes.tolerance = 1.0e-3
+
+
+stokes.petsc_options["snes_monitor"] = None
+stokes.petsc_options["ksp_monitor"] = None
+
+
+stokes.petsc_options["snes_type"] = "newtonls"
+stokes.petsc_options["ksp_type"] = "fgmres"
+
+# stokes.petsc_options.setValue("fieldsplit_velocity_pc_type", "mg")
+stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_type", "kaskade")
+stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_cycle_type", "w")
+
+stokes.petsc_options["fieldsplit_velocity_mg_coarse_pc_type"] = "svd"
+stokes.petsc_options[f"fieldsplit_velocity_ksp_type"] = "fcg"
+stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_type"] = "chebyshev"
+stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_max_it"] = 7
+stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_converged_maxits"] = None
+
+# gasm is super-fast ... but mg seems to be bulletproof
+# gamg is toughest wrt viscosity
+
+stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "gamg")
+stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "additive")
+stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v")
+
+# # # mg, multiplicative - very robust ... similar to gamg, additive
+
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "mg")
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "multiplicative")
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v")
+
+
+stokes._setup_pointwise_functions(verbose=True)
+stokes._setup_discretisation(verbose=True)
+stokes.dm.ds.view()
+
+
+
+
+
+
+
# Solve time
+stokes.solve()
+
+
+stokes._uu_G0
+
+
+# check the mesh if in a notebook / serial
+
+import mpi4py
+
+if mpi4py.MPI.COMM_WORLD.size == 1:
+    import numpy as np
+    import pyvista as pv
+    import vtk
+
+    try:
+        pv.start_xvfb()
+    except OSError:
+        pass
+
+    pv.global_theme.background = "white"
+    pv.global_theme.window_size = [250, 500]
+    pv.global_theme.anti_aliasing = "msaa"
+    pv.global_theme.jupyter_backend = "panel"
+    pv.global_theme.smooth_shading = True
+
+    mesh.vtk("tmp_mesh.vtk")
+    pvmesh = pv.read("tmp_mesh.vtk")
+
+    pvmesh.point_data["P"] = uw.function.evalf(p.sym[0], mesh.data)
+    pvmesh.point_data["V"] = uw.function.evalf(v.sym.dot(v.sym), mesh.data)
+    pvmesh.point_data["delta"] = uw.function.evalf(surface_fn, mesh.data)
+
+    arrow_loc = np.zeros((stokes.u.coords.shape[0], 3))
+    arrow_loc[:, 0:2] = stokes.u.coords[...]
+
+    arrow_length = np.zeros((stokes.u.coords.shape[0], 3))
+    arrow_length[:, 0] = uw.function.evalf(stokes.u.sym[0], stokes.u.coords)
+    arrow_length[:, 1] = uw.function.evalf(stokes.u.sym[1], stokes.u.coords)
+
+    pl = pv.Plotter(window_size=[1000, 1000])
+    pl.add_axes()
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        scalars="V",
+        use_transparency=False,
+        opacity=1.0,
+    )
+
+    pl.add_arrows(arrow_loc, arrow_length, mag=1)
+
+    pl.show(cpos="xy")
+
+
+stokes.bodyforce = sympy.Matrix(
+    [
+        0,
+        -sympy.cos(sympy.pi * x)
+        * sympy.sin(2 * sympy.pi * y)
+        * (1 - (surface_fn + base_fn)),
+    ]
+)
+
+stokes.bodyforce[0] -= 1.0e3 * v.sym[0] * (left_fn + right_fn)
+stokes.bodyforce[1] -= 1.0e3 * v.sym[1] * (surface_fn + base_fn)
+
+viscosity_fn = sympy.Piecewise(
+    (1.0e6, x > x_c),
+    (1.0, True),
+)
+
+stokes.constitutive_model.Parameters.shear_viscosity_0 = viscosity_fn
+
+
+stokes.saddle_preconditioner = sympy.simplify(
+    1 / (stokes.constitutive_model.viscosity + stokes.penalty)
+)
+
+
+stokes._setup_pointwise_functions()
+stokes._setup_discretisation()
+stokes._u_f1
+
+
+timing.reset()
+timing.start()
+
+stokes.solve(zero_init_guess=True)
+
+timing.print_table(display_fraction=0.999)
+
+
+# check the mesh if in a notebook / serial
+
+import mpi4py
+
+if mpi4py.MPI.COMM_WORLD.size == 1:
+    import numpy as np
+    import pyvista as pv
+    import vtk
+
+    pv.global_theme.background = "white"
+    pv.global_theme.window_size = [750, 1200]
+    pv.global_theme.anti_aliasing = "msaa"
+    pv.global_theme.jupyter_backend = "panel"
+    pv.global_theme.smooth_shading = True
+
+    mesh.vtk("tmp_mesh.vtk")
+    pvmesh = pv.read("tmp_mesh.vtk")
+
+    pvmesh.point_data["P"] = uw.function.evalf(p.sym[0], mesh.data)
+    pvmesh.point_data["V"] = uw.function.evalf(v.sym.dot(v.sym), mesh.data)
+
+    arrow_loc = np.zeros((stokes.u.coords.shape[0], 3))
+    arrow_loc[:, 0:2] = stokes.u.coords[...]
+
+    arrow_length = np.zeros((stokes.u.coords.shape[0], 3))
+    arrow_length[:, 0] = uw.function.evalf(stokes.u.sym[0], stokes.u.coords)
+    arrow_length[:, 1] = uw.function.evalf(stokes.u.sym[1], stokes.u.coords)
+
+    pl = pv.Plotter(window_size=[1000, 1000])
+    pl.add_axes()
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        scalars="V",
+        use_transparency=False,
+        opacity=1.0,
+    )
+
+    # pl.add_mesh(pvmesh, cmap="coolwarm", edge_color="Black", show_edges=True, scalars="T",
+    #               use_transparency=False, opacity=1.0)
+
+    pl.add_arrows(arrow_loc, arrow_length, mag=50)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
try:
+    import underworld as uw2
+
+    solC = uw2.function.analytic.SolC()
+    vel_soln_analytic = solC.fn_velocity.evaluate(mesh.data)
+    from mpi4py import MPI
+
+    comm = MPI.COMM_WORLD
+    from numpy import linalg as LA
+
+    with mesh.access(v):
+        num = function.evaluate(v.fn, mesh.data)  # this appears busted
+        if comm.rank == 0:
+            print("Diff norm a. = {}".format(LA.norm(v.data - vel_soln_analytic)))
+            print("Diff norm b. = {}".format(LA.norm(num - vel_soln_analytic)))
+        # if not np.allclose(v.data, vel_soln_analytic, rtol=1):
+        #     raise RuntimeError("Solve did not produce expected result.")
+    comm.barrier()
+except ImportError:
+    import warnings
+
+    warnings.warn("Unable to test SolC results as UW2 not available.")
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/.virtual_documents/Examples-StokesFlow/Readme.html b/main/Notebooks/.virtual_documents/Examples-StokesFlow/Readme.html new file mode 100644 index 0000000..1983d23 --- /dev/null +++ b/main/Notebooks/.virtual_documents/Examples-StokesFlow/Readme.html @@ -0,0 +1,538 @@ + + + + + + + + + + + <no title> — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ + + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Convection/Ex_AdvectionDiffusionSLCN_RotationTest.html b/main/Notebooks/Examples-Convection/Ex_AdvectionDiffusionSLCN_RotationTest.html new file mode 100644 index 0000000..7763046 --- /dev/null +++ b/main/Notebooks/Examples-Convection/Ex_AdvectionDiffusionSLCN_RotationTest.html @@ -0,0 +1,915 @@ + + + + + + + + + + + Field (SemiLagrange) Advection solver test — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Field (SemiLagrange) Advection solver test

+ +
+
+ +
+
+
+ + + + +
+ +
+

Field (SemiLagrange) Advection solver test#

+

Shear flow driven by a pre-defined, rigid body rotation in a disc or by the boundary conditions

+
+
+
import petsc4py
+from petsc4py import PETSc
+
+import os
+import nest_asyncio
+nest_asyncio.apply()
+
+os.environ["UW_TIMING_ENABLE"] = "1"
+
+import underworld3 as uw
+from underworld3.systems import Stokes
+from underworld3 import function
+from underworld3 import VarType
+from underworld3 import timing
+
+import numpy as np
+import sympy
+
+options = PETSc.Options()
+
+# options.getAll()
+
+
+
+
+
+
+
meshball = uw.meshing.Annulus(
+    radiusOuter=1.0, radiusInner=0.5, cellSize=0.2, refinement=1, qdegree=3
+)
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable("U", meshball, meshball.dim, degree=2)
+t_soln = uw.discretisation.MeshVariable("T", meshball, 1, degree=3)
+t_0 = uw.discretisation.MeshVariable("T0", meshball, 1, degree=3, varsymbol=r"T_{0}")
+
+# Create a temperature structure / buoyancy force
+
+import sympy
+
+radius_fn = sympy.sqrt(
+    meshball.rvec.dot(meshball.rvec)
+)  # normalise by outer radius if not 1.0
+unit_rvec = meshball.rvec / (1.0e-10 + radius_fn)
+
+# Some useful coordinate stuff
+
+x, y = meshball.X
+r, th = meshball.CoordinateSystem.xR
+
+# Rigid body rotation v_theta = constant, v_r = 0.0
+
+theta_dot = 2.0 * np.pi  # i.e one revolution in time 1.0
+v_x = -r * theta_dot * sympy.sin(th)
+v_y = r * theta_dot * sympy.cos(th)
+
+with meshball.access(v_soln):
+    v_soln.data[:, 0] = uw.function.evaluate(v_x, v_soln.coords)
+    v_soln.data[:, 1] = uw.function.evaluate(v_y, v_soln.coords)
+
+
+
+
+
+
+
# swarm  = uw.swarm.Swarm(mesh=meshball)
+# T1 = uw.swarm.SwarmVariable("Tminus1", swarm, 1)
+# X1 = uw.swarm.SwarmVariable("Xminus1", swarm, 2)
+# swarm.populate(fill_param=3)
+
+
+
+
+
+
+
# Create adv_diff object
+
+# Set some things
+k = 0.01
+h = 0.1
+t_i = 2.0
+t_o = 1.0
+r_i = 0.5
+r_o = 1.0
+delta_t = 1.0
+
+
+
+
+
+
+
adv_diff = uw.systems.AdvDiffusion(
+    meshball,
+    u_Field=t_soln,
+    V_fn=v_soln,
+    solver_name="adv_diff",
+    order=2,
+)
+
+
+
+
+
+
+
adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel(adv_diff.Unknowns)
+adv_diff.constitutive_model.Parameters.diffusivity = k
+
+
+
+
+
+
+
# Define T boundary conditions via a sympy function
+
+import sympy
+
+abs_r = sympy.sqrt(meshball.rvec.dot(meshball.rvec))
+init_t = sympy.exp(-30.0 * (meshball.N.x**2 + (meshball.N.y - 0.75) ** 2))
+
+adv_diff.add_dirichlet_bc(0.0, "Lower")
+adv_diff.add_dirichlet_bc(0.0, "Upper")
+
+with meshball.access(t_0, t_soln):
+    t_0.data[...] = uw.function.evaluate(init_t, t_0.coords).reshape(-1, 1)
+    t_soln.data[...] = t_0.data[...]
+
+
+
+
+
+
+
# # We can over-ride the swarm-particle update routine since we can integrate
+# # the velocity field by hand.
+
+# with adv_diff._nswarm.access():
+#     coords0 = adv_diff._nswarm.data.copy()
+
+# delta_t = 0.000
+
+# n_x = uw.function.evaluate(r * sympy.cos(th - delta_t * theta_dot), coords0)
+# n_y = uw.function.evaluate(r * sympy.sin(th - delta_t * theta_dot), coords0)
+
+# coords = np.empty_like(coords0)
+# coords[:, 0] = n_x
+# coords[:, 1] = n_y
+
+# # delta_t will be baked in when this is defined ... so re-define it
+# adv_diff.solve(timestep=delta_t) # , coords=coords)
+
+
+
+
+
+
+
def plot_T_mesh(filename):
+    if uw.mpi.size == 1:
+    
+        import pyvista as pv
+        import underworld3.visualisation as vis
+        
+        pvmesh = vis.mesh_to_pv_mesh(meshball)
+        points = vis.meshVariable_to_pv_cloud(t_soln)
+        points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym)
+    
+        point_cloud = pv.PolyData(points)
+    
+        velocity_points = vis.meshVariable_to_pv_cloud(v_soln)
+        velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym)
+        
+        pl = pv.Plotter(window_size=(1000, 750))
+
+        pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.0001, opacity=0.75)
+
+        pl.add_points(
+            point_cloud,
+            cmap="coolwarm",
+            render_points_as_spheres=False,
+            point_size=10,
+            opacity=0.66,
+        )
+
+        pl.add_mesh(pvmesh, "Black", "wireframe", opacity=0.75)
+
+        pl.remove_scalar_bar("T")
+        pl.remove_scalar_bar("mag")
+
+        pl.screenshot(
+            filename="{}.png".format(filename),
+            window_size=(1280, 1280),
+            return_img=False,
+        )
+    
+    pl.show()
+
+
+
+
+
+
+
t_soln2 = uw.discretisation.MeshVariable(
+    "U2", meshball, vtype=uw.VarType.SCALAR, degree=2
+)
+
+
+
+
+
+
+
adv_diff.estimate_dt()
+
+
+
+
+
+
+
adv_diff.DuDt._psi_star_projection_solver._constitutive_model
+
+
+
+
+
+
+
timing.reset()
+timing.start()
+
+delta_t = 0.001
+
+adv_diff.solve(timestep=delta_t, verbose=False, _force_setup=False)
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+
+
+if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshball)
+    points = vis.meshVariable_to_pv_cloud(t_soln)
+    points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym)
+
+    point_cloud = pv.PolyData(points)
+
+    velocity_points = vis.meshVariable_to_pv_cloud(v_soln)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym)
+    
+    pl = pv.Plotter(window_size=(1000, 750))
+
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.01, opacity=0.75)
+    pl.add_points(
+        point_cloud,
+        cmap="coolwarm",
+        render_points_as_spheres=True,
+        point_size=7,
+        opacity=0.66,
+    )
+    pl.add_mesh(pvmesh, "Black", "wireframe", opacity=0.75)
+
+    # pl.remove_scalar_bar("T")
+    pl.remove_scalar_bar("mag")
+
+    pl.show()
+
+
+
+
+
+
+
# Advection/diffusion model / update in time
+
+expt_name = "rotation_test_slcn"
+
+delta_t = 0.05
+
+plot_T_mesh(filename="{}_step_{}".format(expt_name, 0))
+
+for step in range(0, 10):
+    # delta_t will be baked in when this is defined ... so re-define it
+    adv_diff.solve(timestep=delta_t, verbose=False)
+
+    # stats then loop
+
+    # tstats = t_soln.stats()
+
+    if uw.mpi.rank == 0:
+        print("Timestep {}, dt {}".format(step, delta_t))
+        # print(tstats)
+
+    plot_T_mesh(filename="{}_step_{}".format(expt_name, step))
+
+    # savefile = "output_conv/convection_cylinder_{}_iter.h5".format(step)
+    # meshball.save(savefile)
+    # v_soln.save(savefile)
+    # t_soln.save(savefile)
+    # meshball.generate_xdmf(savefile)
+
+
+
+
+
+
+
t_soln.stats()
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+
+if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    
+    pvmesh = vis.mesh_to_pv_mesh(meshball)
+    points = vis.meshVariable_to_pv_cloud(t_soln)
+    points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym)
+    points.point_data["dT"] = vis.scalar_fn_to_pv_points(points, t_soln.sym) - vis.scalar_fn_to_pv_points(points, t_0.sym)
+
+    point_cloud = pv.PolyData(points)
+
+    velocity_points = vis.meshVariable_to_pv_cloud(v_soln)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym)
+    
+    pl = pv.Plotter(window_size=(1000, 750))
+
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.0001, opacity=0.75)
+
+    pl.add_points(
+        point_cloud,
+        cmap="coolwarm",
+        scalars="T",  # clim=[-0.2,0.2],
+        render_points_as_spheres=False,
+        point_size=10,
+        opacity=0.66,
+    )
+
+    pl.add_mesh(pvmesh, "Black", "wireframe", opacity=0.75)
+
+    # pl.remove_scalar_bar("T")
+    pl.remove_scalar_bar("mag")
+
+    pl.show()
+
+
+
+
+
+
+
# savefile = "output_conv/convection_cylinder.h5".format(step)
+# meshball.save(savefile)
+# v_soln.save(savefile)
+# t_soln.save(savefile)
+# meshball.generate_xdmf(savefile)
+
+
+
+
+
+
+
uw.timing.print_table()
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Convection/Ex_AdvectionDiffusionSwarm_RotationTest.html b/main/Notebooks/Examples-Convection/Ex_AdvectionDiffusionSwarm_RotationTest.html new file mode 100644 index 0000000..a2fb725 --- /dev/null +++ b/main/Notebooks/Examples-Convection/Ex_AdvectionDiffusionSwarm_RotationTest.html @@ -0,0 +1,866 @@ + + + + + + + + + + + Swarm Advection solver test - shear flow driven by a pre-defined, rigid body rotation in a disc — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Swarm Advection solver test - shear flow driven by a pre-defined, rigid body rotation in a disc

+ +
+
+ +
+
+
+ + + + +
+ +
+

Swarm Advection solver test - shear flow driven by a pre-defined, rigid body rotation in a disc#

+

This example uses the Swarm advection approach rather than SLCN

+
+
+
import petsc4py
+from petsc4py import PETSc
+
+import nest_asyncio
+nest_asyncio.apply()
+
+import underworld3 as uw
+from underworld3.systems import Stokes
+from underworld3 import function
+
+import numpy as np
+
+options = PETSc.Options()
+# options["help"] = None
+# options["pc_type"]  = "svd"
+# options["dm_plex_check_all"] = None
+
+# import os
+# os.environ["SYMPY_USE_CACHE"]="no"
+
+# options.getAll()
+
+
+
+
+
+
+
import meshio
+
+meshball = uw.meshing.Annulus(
+    radiusOuter=1.0, radiusInner=0.5, cellSize=0.2, refinement=1, qdegree=3
+)
+x, y = meshball.X
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable("U", meshball, meshball.dim, degree=2)
+t_soln = uw.discretisation.MeshVariable("T", meshball, 1, degree=3)
+t_soln_dt = uw.discretisation.MeshVariable("Tdt", meshball, 1, degree=3)
+t_0 = uw.discretisation.MeshVariable("T0", meshball, 1, degree=3)
+
+
+
+
+
+
+
DTdt = uw.systems.Lagrangian_DDt(
+        meshball,
+        psi_fn = t_soln.sym,
+        V_fn = v_soln.sym,
+        vtype = uw.VarType.SCALAR,
+        degree = 1,
+        order = 1,
+        continuous=True,
+        varsymbol=r'T_s',
+        fill_param=3,
+)
+
+
+
+
+
+
+
# check that the swarm variable works  as a continuous field as well
+DTdt.psi_star[0].sym.jacobian(meshball.X)
+
+
+
+
+
+
+
# Create adv_diff object
+
+# Set some things
+k = 0.01
+h = 0.1
+t_i = 2.0
+t_o = 1.0
+r_i = 0.5
+r_o = 1.0
+delta_t = 1.0
+
+
+
+
+
+
+
adv_diff = uw.systems.AdvDiffusion(
+    meshball,
+    u_Field=t_soln,
+    V_fn = v_soln,
+    DuDt = DTdt,
+    solver_name="adv_diff_swarms",  # not needed if coords is provided
+    order=1,
+)
+
+adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel
+adv_diff.constitutive_model.Parameters.diffusivity = k
+
+
+
+
+
+
+
# Create a density structure / bu()oyancy force
+# gravity will vary linearly from zero at the centre
+# of the sphere to (say) 1 at the surface
+
+import sympy
+
+radius_fn = sympy.sqrt(
+    meshball.rvec.dot(meshball.rvec)
+)  # normalise by outer radius if not 1.0
+unit_rvec = meshball.rvec / (1.0e-10 + radius_fn)
+
+# Some useful coordinate stuff
+
+x, y = meshball.X
+r, th = meshball.CoordinateSystem.xR
+
+# Rigid body rotation v_theta = constant, v_r = 0.0
+
+theta_dot = 2.0 * np.pi  # i.e one revolution in time 1.0
+v_x = -r * theta_dot * sympy.sin(th)
+v_y = r * theta_dot * sympy.cos(th)
+
+with meshball.access(v_soln):
+    v_soln.data[:, 0] = uw.function.evaluate(v_x, v_soln.coords)
+    v_soln.data[:, 1] = uw.function.evaluate(v_y, v_soln.coords)
+
+
+
+
+
+
+
# Define T boundary conditions via a sympy function
+
+import sympy
+
+abs_r = sympy.sqrt(meshball.rvec.dot(meshball.rvec))
+
+init_t = sympy.exp(-30.0 * (meshball.N.x**2 + (meshball.N.y - 0.75) ** 2))
+
+adv_diff.add_dirichlet_bc(0.0, "Lower")
+adv_diff.add_dirichlet_bc(0.0, "Upper")
+
+with meshball.access(t_0, t_soln):
+    t_0.data[...] = uw.function.evaluate(init_t, t_0.coords).reshape(-1, 1)
+    t_soln.data[...] = t_0.data[...]
+
+
+
+
+
+
+
# Validation - small timestep
+
+# delta_t = 0.01
+# adv_diff.solve(timestep=delta_t)
+
+
+
+
+
+
+
def plot_T_mesh(filename):
+
+    if uw.mpi.size == 1:
+
+        import numpy as np
+        import pyvista as pv
+        import underworld3.visualisation as vis
+        
+        pvmesh = vis.mesh_to_pv_mesh(meshball)
+        swarm_points = vis.swarm_to_pv_cloud(adv_diff.DuDt.swarm)
+        tsoln_points = vis.meshVariable_to_pv_cloud(t_soln)
+            
+        swarm_points.point_data["T"] = vis.scalar_fn_to_pv_points(swarm_points,adv_diff.DuDt.psi_fn)
+        
+        pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh,t_soln.sym)
+        pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh,v_soln.sym)
+
+        pl = pv.Plotter(window_size=(1000, 750))
+
+        pl.add_arrows(pvmesh.points, pvmesh.point_data["V"], mag=0.0001, opacity=0.75)
+
+        # pl.add_points(
+        #     swarm_points,
+        #     cmap="coolwarm",
+        #     render_points_as_spheres=False,
+        #     point_size=20,
+        #     opacity=0.66,
+        # )
+    
+        pl.add_mesh(pvmesh, cmap="coolwarm", opacity=0.75)
+    
+        pl.screenshot(
+            filename="{}.png".format(filename),
+            window_size=(1280, 1280),
+            return_img=False,
+        )
+
+    # pl.show()
+
+
+
+
+
+
+
with meshball.access(t_0, t_soln):
+    t_0.data[...] = uw.function.evaluate(init_t, t_0.coords).reshape(-1, 1)
+    t_soln.data[...] = t_0.data[...]
+
+
+
+
+
+
+
+
adv_diff.DuDt.update(dt=0.05)
+
+# # Update the swarm locations
+# swarm.advection(
+#     v_soln.sym,
+#     delta_t=0.05,
+#     corrector=False,
+#     restore_points_to_domain_func=meshball.return_coords_to_bounds,
+# )  
+
+
+
+
+
+
+
# Advection/diffusion model / update in time
+
+delta_t = 0.05
+expt_name = "output/rotation_test_k_001"
+
+plot_T_mesh(filename="{}_step_{}".format(expt_name, 0))
+
+for step in range(0, 10):
+
+    adv_diff.solve(timestep=delta_t, verbose=False)
+
+    tstats = t_soln.stats()
+    print("psi*", adv_diff.DuDt.psi_star[0]._meshVar.stats())
+
+    if uw.mpi.rank == 0:
+        print("Timestep {}, dt {}".format(step, delta_t))
+        print(tstats)
+
+    plot_T_mesh(filename="{}_step_{}".format(expt_name, step))
+
+    # savefile = "output_conv/convection_cylinder_{}_iter.h5".format(step)
+    # meshball.save(savefile)
+    # v_soln.save(savefile)
+    # t_soln.save(savefile)
+    # meshball.generate_xdmf(savefile)
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+
+
+if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshball)
+    swarm_points = vis.swarm_to_pv_cloud(adv_diff.DuDt.swarm)
+    tsoln_points = vis.meshVariable_to_pv_cloud(t_soln)
+        
+    swarm_points.point_data["Ts"] = vis.scalar_fn_to_pv_points(swarm_points, adv_diff.DuDt.psi_star[0].sym[0] )
+
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh,t_soln.sym)
+    pvmesh.point_data["Ts"] = vis.scalar_fn_to_pv_points(pvmesh,adv_diff.DuDt.psi_star[0].sym[0])
+    pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh,v_soln.sym)
+
+    pl = pv.Plotter(window_size=(1000, 750))
+
+    pl.add_arrows(pvmesh.points, pvmesh.point_data["V"], mag=0.02, opacity=0.75)
+
+    pl.add_points(
+        swarm_points,
+        cmap="coolwarm",
+        render_points_as_spheres=False,
+        scalars="Ts",
+        point_size=10,
+        opacity=0.66,
+    )
+
+    # pl.add_mesh(pvmesh, cmap="coolwarm", opacity=0.75, scalars="T")
+
+    # pl.remove_scalar_bar("T")
+    # pl.remove_scalar_bar("mag")
+
+    pl.show()
+
+
+
+
+
+
+
# savefile = "output_conv/convection_cylinder.h5".format(step)
+# meshball.save(savefile)
+# v_soln.save(savefile)
+# t_soln.save(savefile)
+# meshball.generate_xdmf(savefile)
+
+
+
+
+
+
+
DTdt.psi_fn
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Convection/Ex_AdvectionDiffusion_1dBlock.html b/main/Notebooks/Examples-Convection/Ex_AdvectionDiffusion_1dBlock.html new file mode 100644 index 0000000..0b58f53 --- /dev/null +++ b/main/Notebooks/Examples-Convection/Ex_AdvectionDiffusion_1dBlock.html @@ -0,0 +1,948 @@ + + + + + + + + + + + Advection-diffusion of a hot pipe — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + +
+ +
+

Advection-diffusion of a hot pipe#

+
    +
  • Using the adv_diff solver.

  • +
  • Advection of the rectangular pulse vertically as it also diffuses. The velocity is 0.05 and has a diffusivity value of 1, 0.1 or 0.01

  • +
  • Benchmark comparison between 1D analytical solution and 2D UW numerical model.

  • +
+

+

Figure: typical results from this test. Quad mesh v. unstructured triangles with equivalent +resolution. \(\kappa=1\), \(\mathbf{v}=(1000,0)\), \(t_0 = 0.0001\), \(\delta t = 0.0003\). The error looks significantly larger with triangles but you can see that it is dominated by a relatively small phase error where the speed of propagation is slightly different from the analytic case.

+
+

How to test advection or diffusion only#

+
    +
  • Set velocity to 0 to test diffusion only.

  • +
  • Set diffusivity (k) to 0 to test advection only.

  • +
+
+
+

Analytic solution#

+
+\[ +T(x,t) = +\frac{\operatorname{erf}{\left(\frac{- \mathrm{x} + v \left(t + {t_0}\right) + \frac{{\delta}}{2} + {x_0}}{2 \sqrt{\kappa \left(t + {t_0}\right)}} \right)}}{2} + \frac{\operatorname{erf}{\left(\frac{\mathrm{x} - v \left(t + {t_0}\right) + \frac{{\delta}}{2} - {x_0}}{2 \sqrt{\kappa \left(t + {t_0}\right)}} \right)}}{2} +\]
+

Where \(x,y\) describe the coordinate frame, \(v\) is the horizontal velocity that advects the temperature, \(\delta\) is the width of the temperature anomaly, \(x_0\) is the initial midpoint of the temperature anomaly. \(\kappa\) is the thermal diffusivity, \(t_0\) is the time at which we turn on the horizontal velocity.

+

Note: this solution is derived from the diffusion of a step which is applied to the leading and trailing edges of the block. The solution is valid while the diffusion fronts from each interface remain independent of each other. (This is ill-defined from the problem, but the most obvious test is to look a the time that the block temperature drops below 1 to the tolerance of the solver).

+
+
+
import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import underworld3 as uw
+import numpy as np
+import sympy
+import math
+import os
+
+from scipy import special
+
+
+
+
+
+
+
if uw.mpi.size == 1:
+    import matplotlib.pyplot as plt
+
+
+
+
+
+

Set up variables of the model#

+
+
+
import sys
+
+init_t = 0.0001
+dt   = 0.0003
+velocity = 1000.
+centre = 0.2
+width = 0.2
+
+
+### min and max temps
+tmin = 0.  # temp min
+tmax = 1.0 # temp max
+
+# I think we should get into the habit of doing this consistently with the PETSc interface
+
+res = uw.options.getReal("model_resolution", default=16)
+kappa = uw.options.getInt("kappa", default=1.0)
+Tdegree = uw.options.getInt("Tdeg", default=3)
+Vdegree = uw.options.getInt("Vdeg", default=2)
+simplex = uw.options.getBool("simplex", default=True)
+
+
+
+# Tdegree = int(sys.argv[1])
+# Vdegree = int(sys.argv[2])
+# kappa = float(sys.argv[3]) # 1, 0.1, 0.01 # diffusive constant
+# res = int(sys.argv[4])
+# simplex = sys.argv[5].lower()
+
+
+
+
+
+
+
outputPath = f'./output/adv_diff-hot_pipe/'
+
+if uw.mpi.rank == 0:
+    # checking if the directory
+    if not os.path.exists(outputPath):
+        os.makedirs(outputPath)
+
+
+
+
+
+
+

Set up the mesh#

+
+
+
xmin, xmax = 0, 1
+ymin, ymax = 0, 0.2
+
+
+
+
+
+
+
## Quads
+if simplex == True:
+    mesh = uw.meshing.UnstructuredSimplexBox(
+    minCoords=(xmin, ymin), maxCoords=(xmax, ymax), cellSize=(ymax-ymin)/res, regular=False, qdegree=max(Tdegree, Vdegree) )
+else:
+    mesh = uw.meshing.StructuredQuadBox(
+        elementRes=(int(res)*5, int(res)), minCoords=(xmin, ymin), maxCoords=(xmax, ymax), qdegree=max(Tdegree, Vdegree),
+    )
+
+
+
+
+
+
+

Create mesh variables#

+

To be used in the solver

+
+
+
x,y = mesh.X
+
+x0 = sympy.symbols(r"{x_0}")
+t0 = sympy.symbols(r"{t_0}")
+delta = sympy.symbols(r"{\delta}")
+ks = sympy.symbols(r"\kappa")
+ts = sympy.symbols("t")
+vs = sympy.symbols("v")
+
+Ts =  ( sympy.erf( (x0 + delta/2  - x+(vs*(ts+t0)))  / (2*sympy.sqrt(ks*(ts+t0)))) + sympy.erf( (-x0 + delta/2 + x-((ts+t0)*vs))  / (2*sympy.sqrt(ks*(ts+t0)))) ) / 2
+Ts
+
+
+
+
+
+
+
def build_analytic_fn_at_t(time): 
+    fn = Ts.subs({vs:velocity, ts:time, ks:kappa, delta:width, x0:centre, t0:init_t})
+    return fn
+
+
+Ts0 = build_analytic_fn_at_t(time=0.0)
+TsVKT = build_analytic_fn_at_t(time=dt)
+
+
+
+
+
+
+
# Create the mesh var
+T       = uw.discretisation.MeshVariable("T", mesh, 1, degree=Tdegree)
+
+# This is the velocity field
+
+v = sympy.Matrix([velocity, 0])
+
+
+
+
+
+

Create the advDiff solver#

+
+
+
adv_diff = uw.systems.AdvDiffusionSLCN(
+    mesh,
+    u_Field=T,
+    V_fn=v,
+    solver_name="adv_diff",
+)
+
+
+
+
+
+
+
+

Set up properties of the adv_diff solver#

+
    +
  • Constitutive model (Diffusivity)

  • +
  • Boundary conditions

  • +
  • Internal velocity

  • +
  • Initial temperature distribution

  • +
+
+
+
adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel
+adv_diff.constitutive_model.Parameters.diffusivity = kappa
+
+
+
+
+
+
+
adv_diff.add_dirichlet_bc(tmin, "Left")
+adv_diff.add_dirichlet_bc(tmin, "Right")
+
+
+
+
+
+
+
adv_diff.estimate_dt(v_factor=10)
+steps = int(dt // (10*adv_diff.estimate_dt()))
+
+
+
+
+
+
+

Create points to sample the UW results#

+
+
+
### get the initial temp profile
+
+with mesh.access(T):
+    T.data[:,0] = uw.function.evalf(Ts0, T.coords)
+
+
+
+
+
+
+
step = 0
+model_time = 0.0
+
+
+
+
+
+
+
adv_diff.petsc_options["snes_monitor_short"] = None
+
+# if uw.mpi.size == 1:
+#     adv_diff.petsc_options['pc_type'] = 'lu'
+    
+
+
+
+
+
+
+
while model_time < dt:    
+    adv_diff.solve(timestep=dt/steps, zero_init_guess=False)
+    model_time += dt/steps
+    step += 1
+    print(f"Timestep: {step}, model time {model_time}")
+
+
+
+
+
+
+
# %%
+if uw.mpi.size == 1:
+
+        import pyvista as pv
+        import underworld3.visualisation as vis
+
+        pvmesh = vis.mesh_to_pv_mesh(mesh)
+        pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, sympy.Matrix([velocity, 0]).T)
+        pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, T.sym)
+        pvmesh.point_data["Ta"] = vis.scalar_fn_to_pv_points(pvmesh, Ts0)
+        pvmesh.point_data["dT"] = pvmesh.point_data["T"] - pvmesh.point_data["Ta"]
+
+        T_points = vis.meshVariable_to_pv_cloud(T)
+        T_points.point_data["T"] = vis.scalar_fn_to_pv_points(T_points, T.sym)
+        T_points.point_data["Ta"] = vis.scalar_fn_to_pv_points(T_points, TsVKT)
+        T_points.point_data["T0"] = vis.scalar_fn_to_pv_points(T_points, Ts0)
+        T_points.point_data["Tp"] = (T_points.point_data["T0"] + T_points.point_data["Ta"])/2
+        T_points.point_data["dT"] = T_points.point_data["T"] - T_points.point_data["Ta"]
+
+        pvmesh2 = vis.mesh_to_pv_mesh(mesh)
+        pvmesh2.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh2, T.sym)
+        pvmesh2.point_data["T0"] = vis.scalar_fn_to_pv_points(pvmesh2, Ts0)
+        pvmesh2.points[:,1] += 0.3        
+    
+        pvmesh3 = vis.mesh_to_pv_mesh(mesh)
+        pvmesh3.point_data["Ta"] = vis.scalar_fn_to_pv_points(pvmesh2, TsVKT)
+        pvmesh3.points[:,1] -= 0.3
+
+
+        pl = pv.Plotter()
+
+        pl.add_mesh(
+            pvmesh2,
+            cmap="coolwarm",
+            edge_color="Black",
+            show_edges=True,
+            scalars="T",
+            use_transparency=False,
+            show_scalar_bar=False,
+            opacity=1,
+        )
+
+
+        pl.add_mesh(
+                    
+            pvmesh3,
+            cmap="coolwarm",
+            edge_color="Black",
+            show_edges=True,
+            scalars="Ta",
+            use_transparency=False,
+            show_scalar_bar=False,
+            opacity=1,
+        )
+
+        pl.add_points(T_points, color="White",
+                      scalars="dT", cmap="coolwarm",
+                      point_size=5.0, opacity=0.5)
+
+
+        pl.add_arrows(pvmesh.points, pvmesh.point_data["V"], mag=0.00003, opacity=0.5, show_scalar_bar=False)
+
+        # pl.add_points(pdata)
+
+        pl.show(cpos="xy")
+
+        # return vsol
+
+
+
+
+
+
+
T_points.point_data["dT"].max()
+
+
+
+
+
+
+
T_points.point_data["Ta"].max()
+
+
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Convection/Ex_Convection_1_SLCN_Cartesian.html b/main/Notebooks/Examples-Convection/Ex_Convection_1_SLCN_Cartesian.html new file mode 100644 index 0000000..9b812a2 --- /dev/null +++ b/main/Notebooks/Examples-Convection/Ex_Convection_1_SLCN_Cartesian.html @@ -0,0 +1,936 @@ + + + + + + + + + + + Constant viscosity convection, Cartesian domain (benchmark) — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Constant viscosity convection, Cartesian domain (benchmark)

+ +
+
+ +
+
+
+ + + + +
+ +
+

Constant viscosity convection, Cartesian domain (benchmark)#

+

This is a simple example in which we try to instantiate two solvers on the mesh and have them use a common set of variables.

+

We set up a v, p, T system in which we will solve for a steady-state T field in response to thermal boundary conditions and then use the steady-state T field to compute a stokes flow in response.

+

The next step is to add particles at node points and sample back along the streamlines to find values of the T field at a previous time.

+

(Note, we keep all the pieces from previous increments of this problem to ensure that we don’t break something along the way)

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+
+import underworld3 as uw
+from underworld3.systems import Stokes
+from underworld3 import function
+
+import numpy as np
+import sympy
+
+
+
+
+
+
+
meshbox = uw.meshing.UnstructuredSimplexBox(
+    minCoords=(0.0, 0.0),
+    maxCoords=(1.0, 1.0),
+    cellSize=1.0 / 32.0,
+    regular=False,
+    qdegree=3,
+)
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+
+if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    # pv.start_xvfb()
+
+    pvmesh = vis.mesh_to_pv_mesh(meshbox)
+    pl = pv.Plotter(window_size=(1000, 750))
+
+    # pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.5)
+    pl.add_mesh(pvmesh, edge_color="Black", show_edges=True)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable("U", meshbox, meshbox.dim, degree=2)
+p_soln = uw.discretisation.MeshVariable("P", meshbox, 1, degree=1)
+t_soln = uw.discretisation.MeshVariable("T", meshbox, 1, degree=3)
+t_0 = uw.discretisation.MeshVariable("T0", meshbox, 1, degree=3)
+
+
+
+
+
+
+
# Create Stokes object
+
+stokes = Stokes(
+    meshbox,
+    velocityField=v_soln,
+    pressureField=p_soln,
+    solver_name="stokes",
+)
+
+# Constant viscosity
+
+viscosity = 1
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.shear_viscosity_0 = 1.0
+stokes.tolerance = 1.0e-3
+
+
+# free slip.
+# note with petsc we always need to provide a vector of correct cardinality.
+
+stokes.add_dirichlet_bc((sympy.oo,0.0), "Bottom")
+stokes.add_dirichlet_bc((sympy.oo, 0.0), "Top")
+stokes.add_dirichlet_bc((0.0,sympy.oo), "Left")
+stokes.add_dirichlet_bc((0.0,sympy.oo), "Right")
+
+
+
+
+
+
+
# Create a density structure / buoyancy force
+# gravity will vary linearly from zero at the centre
+# of the sphere to (say) 1 at the surface
+
+import sympy
+
+# Some useful coordinate stuff
+
+x, y = meshbox.X
+
+
+
+
+
+
+
# Create adv_diff object
+
+# Set some things
+k = 1.0
+h = 0.0
+
+adv_diff = uw.systems.AdvDiffusionSLCN(
+    meshbox,
+    u_Field=t_soln,
+    V_fn=v_soln,
+    solver_name="adv_diff",
+)
+
+adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel
+adv_diff.constitutive_model.Parameters.diffusivity = k
+adv_diff.theta = 0.5
+
+
+
+
+
+
+
# Define T boundary conditions via a sympy function
+
+import sympy
+
+init_t = 0.01 * sympy.sin(5.0 * x) * sympy.sin(np.pi * y) + (1.0 - y)
+
+adv_diff.add_dirichlet_bc(1.0, "Bottom")
+adv_diff.add_dirichlet_bc(0.0, "Top")
+
+with meshbox.access(t_0, t_soln):
+    t_0.data[...] = uw.function.evaluate(init_t, t_0.coords, meshbox.N).reshape(-1, 1)
+    t_soln.data[...] = t_0.data[...]
+
+
+
+
+
+
+
buoyancy_force = 1.0e6 * t_soln.sym[0]
+stokes.bodyforce = sympy.Matrix([0, buoyancy_force])
+
+
+
+
+
+
+
# check the stokes solve is set up and that it converges
+stokes.solve(zero_init_guess=True)
+
+
+
+
+
+
+
# Check the diffusion part of the solve converges
+adv_diff.solve(timestep=0.1 * stokes.estimate_dt(), zero_init_guess=True)
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+
+if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshbox)
+    pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)/10
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+    points = vis.meshVariable_to_pv_cloud(t_soln)
+    point_cloud = pv.PolyData(points)
+    point_cloud.point_data["Tp"] = vis.scalar_fn_to_pv_points(points, t_soln.sym)
+
+    # points = np.zeros((t_soln.coords.shape[0], 3))
+    # points[:, 0] = t_soln.coords[:, 0]
+    # points[:, 1] = t_soln.coords[:, 1]
+
+    # point_cloud = pv.PolyData(points)
+
+    # with meshbox.access():
+    #     point_cloud.point_data["Tp"] = t_soln.data.copy()
+
+    # point sources at cell centres
+    cpoints = np.zeros((meshbox._centroids.shape[0] // 4, 3))
+    cpoints[:, 0] = meshbox._centroids[::4, 0]
+    cpoints[:, 1] = meshbox._centroids[::4, 1]
+    cpoint_cloud = pv.PolyData(cpoints)
+
+    pvstream = pvmesh.streamlines_from_source(
+        cpoint_cloud,
+        vectors="V",
+        integrator_type=2,
+        integration_direction="forward",
+        compute_vorticity=False,
+        max_steps=1000,
+        surface_streamlines=True,
+    )
+
+    pl = pv.Plotter(window_size=(1000, 750))
+
+    # pl.add_mesh(pvmesh,'Gray', 'wireframe')
+
+    # pl.add_mesh(
+    #     pvmesh, cmap="coolwarm", edge_color="Black",
+    #     show_edges=True, scalars="T", use_transparency=False, opacity=0.5,
+    # )
+
+    pl.add_points(
+        point_cloud,
+        cmap="coolwarm",
+        render_points_as_spheres=True,
+        point_size=10,
+        opacity=0.33,
+    )
+
+    pl.add_mesh(pvstream, opacity=0.5)
+    # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1)
+
+    # pl.add_points(pdata)
+
+    pl.show(cpos="xy")
+    pvmesh.clear_data()
+    pvmesh.clear_point_data()
+
+
+
+
+
+
+
pvmesh.clear_point_data()
+
+
+
+
+
+
+
def plot_T_mesh(filename):
+    if uw.mpi.size == 1:
+        
+        import pyvista as pv
+        import underworld3.visualisation as vis
+
+        pvmesh = vis.mesh_to_pv_mesh(meshbox)
+        pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)/333
+        pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+
+        # point sources at cell centres
+        cpoints = np.zeros((meshbox._centroids.shape[0] // 4, 3))
+        cpoints[:, 0] = meshbox._centroids[::4, 0]
+        cpoints[:, 1] = meshbox._centroids[::4, 1]
+        cpoint_cloud = pv.PolyData(cpoints)
+
+        pvstream = pvmesh.streamlines_from_source(
+                                                    cpoint_cloud,
+                                                    vectors="V",
+                                                    integrator_type=45,
+                                                    integration_direction="forward",
+                                                    compute_vorticity=False,
+                                                    max_steps=25,
+                                                    surface_streamlines=True,
+                                                )
+
+        points = vis.meshVariable_to_pv_cloud(t_soln)
+        points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym)
+        point_cloud = pv.PolyData(points)
+
+        pl = pv.Plotter(window_size=(1000, 750))
+
+        pl.add_mesh(
+                    pvmesh,
+                    cmap="coolwarm",
+                    edge_color="Gray",
+                    show_edges=True,
+                    scalars="T",
+                    use_transparency=False,
+                    opacity=0.5,
+                )
+
+        pl.add_points(
+                        point_cloud,
+                        cmap="coolwarm",
+                        render_points_as_spheres=False,
+                        point_size=10,
+                        opacity=0.5,
+                    )
+
+        pl.add_mesh(pvstream, opacity=0.4)
+
+        pl.remove_scalar_bar("T")
+        pl.remove_scalar_bar("V")
+
+        pl.screenshot(
+            filename="{}.png".format(filename),
+            window_size=(1280, 1280),
+            return_img=False,
+        )
+        # pl.show()
+        pl.close()
+
+        pvmesh.clear_data()
+        pvmesh.clear_point_data()
+
+        pv.close_all()
+
+
+
+
+
+
+
t_step = 0
+
+
+
+
+
+
+
# Convection model / update in time
+
+##
+## There is a strange interaction here between the solvers if the zero_guess is
+## set to False
+##
+
+expt_name = "output/Ra1e6"
+
+for step in range(0, 50):
+    stokes.solve(zero_init_guess=True)
+    delta_t = 5.0 * stokes.estimate_dt()
+    adv_diff.solve(timestep=delta_t, zero_init_guess=False)
+
+    # stats then loop
+    tstats = t_soln.stats()
+
+    if uw.mpi.rank == 0:
+        print("Timestep {}, dt {}".format(step, delta_t))
+    #         print(tstats)
+
+    if t_step % 5 == 0:
+        plot_T_mesh(filename="{}_step_{}".format(expt_name, t_step))
+
+    t_step += 1
+
+
+
+
+

savefile = “output_conv/convection_cylinder.h5”.format(step) +meshbox.save(savefile) +v_soln.save(savefile) +t_soln.save(savefile) +meshbox.generate_xdmf(savefile)

+
+
+
if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh =  vis.mesh_to_pv_mesh(meshbox)
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+    pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, stokes.u.sym)
+
+    pl = pv.Plotter(window_size=(1000, 750))
+
+    # pl.add_arrows(arrow_loc, arrow_length, mag=0.00002, opacity=0.75)
+    # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1)
+
+    # pl.add_points(point_cloud, cmap="coolwarm", render_points_as_spheres=False, point_size=7.5, opacity=0.25)
+
+    pl.add_mesh(pvmesh, cmap="coolwarm", scalars="T", opacity=0.75)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Convection/Ex_Convection_2_SLCN_Cartesian-TdepVisc.html b/main/Notebooks/Examples-Convection/Ex_Convection_2_SLCN_Cartesian-TdepVisc.html new file mode 100644 index 0000000..b7254d1 --- /dev/null +++ b/main/Notebooks/Examples-Convection/Ex_Convection_2_SLCN_Cartesian-TdepVisc.html @@ -0,0 +1,861 @@ + + + + + + + + + + + Temperature-dependent viscosity convection, Cartesian Domain(benchmark) — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Temperature-dependent viscosity convection, Cartesian Domain(benchmark)

+ +
+
+ +
+
+
+ + + + +
+ +
+

Temperature-dependent viscosity convection, Cartesian Domain(benchmark)#

+

This is a simple example in which we try to instantiate two solvers on the mesh and have them use a common set of variables.

+

We set up a v, p, T system in which we will solve for a steady-state T field in response to thermal boundary conditions and then use the steady-state T field to compute a stokes flow in response.

+

The next step is to add particles at node points and sample back along the streamlines to find values of the T field at a previous time.

+

(Note, we keep all the pieces from previous increments of this problem to ensure that we don’t break something along the way)

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+
+import underworld3 as uw
+from underworld3.systems import Stokes
+from underworld3 import function
+
+import numpy as np
+import sympy
+
+
+
+
+
+
+
meshbox = uw.meshing.UnstructuredSimplexBox(
+    minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0), cellSize=1.0 / 32.0, qdegree=3
+)
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable("U", meshbox, meshbox.dim, degree=2)
+p_soln = uw.discretisation.MeshVariable("P", meshbox, 1, degree=1, continuous=True)
+t_soln = uw.discretisation.MeshVariable("T", meshbox, 1, degree=3)
+t_0 = uw.discretisation.MeshVariable("T0", meshbox, 1, degree=3)
+
+
+
+
+
+
+
# Create Stokes object
+
+stokes = Stokes(
+    meshbox,
+    velocityField=v_soln,
+    pressureField=p_soln,
+    solver_name="stokes",
+)
+
+# Set solve options here (or remove default values
+# stokes.petsc_options.getAll()
+# stokes.petsc_options.delValue("ksp_monitor")
+# stokes.petsc_options["snes_test_jacobian"] = None
+
+# T dependent visc
+log10_delta_eta = 6
+delta_eta = 10**log10_delta_eta
+
+stokes.petsc_options["snes_rtol"] = 1 / delta_eta
+stokes.petsc_options["snes_atol"] = 0.01  # Based on how the scaling works
+
+viscosity = delta_eta * sympy.exp(-sympy.log(delta_eta) * t_soln.sym[0])
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.viscosity = viscosity
+stokes.penalty = 0.0
+
+stokes.saddle_preconditioner = 1.0 / viscosity
+
+# Velocity boundary conditions
+stokes.add_dirichlet_bc((0.0,), "Top", (1,))
+stokes.add_dirichlet_bc((0.0,), "Bottom", (1,))
+stokes.add_dirichlet_bc((0.0,), "Left", (0,))
+stokes.add_dirichlet_bc((0.0,), "Right", (0,))
+
+
+
+
+
+
+
# Create a density structure / buoyancy force
+# gravity will vary linearly from zero at the centre
+# of the sphere to (say) 1 at the surface
+
+import sympy
+
+# Some useful coordinate stuff
+
+x, y = meshbox.X
+
+
+
+
+
+
+
# Create adv_diff object
+
+# Set some things
+k = 1.0
+h = 0.0
+
+adv_diff = uw.systems.AdvDiffusionSLCN(
+    meshbox,
+    u_Field=t_soln,
+    V_fn=v_soln,
+    solver_name="adv_diff",
+)
+
+adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel
+adv_diff.constitutive_model.Parameters.diffusivity = k
+adv_diff.theta = 0.5
+
+
+
+
+
+
+
# Define T boundary conditions via a sympy function
+
+import sympy
+
+init_t = 0.9 * (0.05 * sympy.cos(sympy.pi * x) + sympy.cos(0.5 * np.pi * y)) + 0.05
+
+adv_diff.add_dirichlet_bc(1.0, "Bottom")
+adv_diff.add_dirichlet_bc(0.0, "Top")
+
+with meshbox.access(t_0, t_soln):
+    t_0.data[...] = uw.function.evaluate(init_t, t_0.coords).reshape(-1, 1)
+    t_soln.data[...] = t_0.data[...]
+    print(t_0.data.max(), t_0.data.min())
+
+
+
+
+
+
+
buoyancy_force = 1.0e6 * t_soln.sym[0]
+stokes.bodyforce = sympy.Matrix([0, buoyancy_force])
+
+
+
+
+
+
+
# check the stokes solve is set up and that it converges
+stokes.solve(zero_init_guess=True)
+
+
+
+
+
+
+
# Check the diffusion part of the solve converges
+adv_diff.solve(timestep=0.1 * stokes.estimate_dt())
+
+
+
+
+
+
+
def plot_T_mesh(filename):
+    if uw.mpi.size == 1:
+        
+        import pyvista as pv
+        import underworld3.visualisation as vis
+
+        pvmesh =  vis.mesh_to_pv_mesh(meshbox)
+        pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)/333
+        pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+
+        # point sources at cell centres
+        cpoints = np.zeros((meshbox._centroids[::4, 0].shape[0], 3))
+        cpoints[:, 0] = meshbox._centroids[::4, 0]
+        cpoints[:, 1] = meshbox._centroids[::4, 1]
+        cpoint_cloud = pv.PolyData(cpoints)
+
+        pvstream = pvmesh.streamlines_from_source(
+            cpoint_cloud,
+            vectors="V",
+            integrator_type=45,
+            integration_direction="forward",
+            compute_vorticity=False,
+            max_steps=25,
+            surface_streamlines=True,
+        )
+
+        points = vis.meshVariable_to_pv_cloud(t_soln)
+        points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym)
+        point_cloud = pv.PolyData(points)
+
+        ## PLOTTING
+        pl = pv.Plotter(window_size=(1000, 750))
+        
+        pl.add_mesh(
+            pvmesh,
+            cmap="coolwarm",
+            edge_color="Gray",
+            show_edges=True,
+            scalars="T",
+            use_transparency=False,
+            opacity=0.5,
+        )
+
+        pl.add_points(
+            point_cloud,
+            cmap="coolwarm",
+            render_points_as_spheres=False,
+            point_size=10,
+            opacity=0.5,
+        )
+
+        pl.add_mesh(pvstream, opacity=0.4)
+
+        pl.remove_scalar_bar("T")
+        pl.remove_scalar_bar("V")
+
+        pl.screenshot(
+            filename="{}.png".format(filename),
+            window_size=(1280, 1280),
+            return_img=False,
+        )
+        # pl.show()
+
+        pvmesh.clear_data()
+        pvmesh.clear_point_data()
+
+
+
+
+
+
+
t_step = 0
+
+
+
+
+
+
+
# Convection model / update in time
+
+##
+## There is a strange interaction here between the solvers if the zero_guess is
+## set to False
+##
+
+expt_name = f"output/Ra1e6_eta1e{log10_delta_eta}"
+
+for step in range(0, 1000):
+    stokes.solve(zero_init_guess=False)
+    delta_t = 5.0 * stokes.estimate_dt()
+    adv_diff.solve(timestep=delta_t, zero_init_guess=True)
+
+    # stats then loop
+    tstats = t_soln.stats()
+
+    if uw.mpi.rank == 0:
+        print("Timestep {}, dt {}".format(step, delta_t))
+    #         print(tstats)
+
+    if t_step % 5 == 0:
+        plot_T_mesh(filename="{}_step_{}".format(expt_name, t_step))
+
+    t_step += 1
+
+# savefile = "{}_ts_{}.h5".format(expt_name,step)
+# meshbox.save(savefile)
+# v_soln.save(savefile)
+# t_soln.save(savefile)
+# meshbox.generate_xdmf(savefile)
+
+pass
+
+
+
+
+

savefile = “output_conv/convection_cylinder.h5”.format(step) +meshbox.save(savefile) +v_soln.save(savefile) +t_soln.save(savefile) +meshbox.generate_xdmf(savefile)

+
+
+
if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh =  vis.mesh_to_pv_mesh(meshbox)
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+
+    velocity_points = vis.meshVariable_to_pv_cloud(stokes.u)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym)
+
+    points = vis.meshVariable_to_pv_cloud(t_soln)
+    points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym)
+    point_cloud = pv.PolyData(points)
+
+    pl = pv.Plotter(window_size=(1000, 750))
+
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.00001, opacity=0.75)
+    # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1)
+
+    pl.add_points(
+        point_cloud,
+        cmap="coolwarm",
+        render_points_as_spheres=True,
+        point_size=7,
+        opacity=0.25,
+    )
+
+    pl.add_mesh(pvmesh, "Black", "wireframe", opacity=0.75)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Convection/Ex_Convection_3_SLCN_Cylindrical-TdepVisc.html b/main/Notebooks/Examples-Convection/Ex_Convection_3_SLCN_Cylindrical-TdepVisc.html new file mode 100644 index 0000000..b004030 --- /dev/null +++ b/main/Notebooks/Examples-Convection/Ex_Convection_3_SLCN_Cylindrical-TdepVisc.html @@ -0,0 +1,982 @@ + + + + + + + + + + + Temperature-dependent viscosity convection, Cylindrical domain (benchmark) — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Temperature-dependent viscosity convection, Cylindrical domain (benchmark)

+ +
+
+ +
+
+
+ + + + +
+ +
+

Temperature-dependent viscosity convection, Cylindrical domain (benchmark)#

+

This is a simple example in which we try to instantiate two solvers on the mesh and have them use a common set of variables.

+

We set up a v, p, T system in which we will solve for a steady-state T field in response to thermal boundary conditions and then use the steady-state T field to compute a stokes flow in response.

+

The next step is to add particles at node points and sample back along the streamlines to find values of the T field at a previous time.

+

This has a free-slip lower boundary and a fixed upper boundary (simplifies the null space, and the lid is stagnant anyway)

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+
+import underworld3 as uw
+from underworld3.systems import Stokes
+from underworld3 import function
+
+import numpy as np
+import sympy
+
+
+
+
+
+
+
# Parameters
+
+r_o = 1.0
+r_i = 0.5
+res = 1 / 24
+
+Rayleigh = 1.0e6 / (r_o - r_i) ** 3
+
+log10_delta_eta = 4
+
+
+
+
+
+
+
# Visualisation
+
+import pyvista as pv
+
+pv.global_theme.background = "white"
+pv.global_theme.window_size = [750, 250]
+pv.global_theme.anti_aliasing = "msaa"
+pv.global_theme.jupyter_backend = "trame"
+pv.global_theme.smooth_shading = True
+
+
+
+
+
+
+
meshdisc = uw.meshing.Annulus(
+    radiusOuter=r_o,
+    radiusInner=r_i,
+    cellSize=float(res),
+    qdegree=3,
+)
+
+#
+meshdisc.vtk("tmp_ann_mesh.vtk")
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable("U", meshdisc, meshdisc.dim, degree=2)
+p_soln = uw.discretisation.MeshVariable("P", meshdisc, 1, degree=1, continuous=True)
+t_soln = uw.discretisation.MeshVariable("T", meshdisc, 1, degree=3)
+meshr = uw.discretisation.MeshVariable(r"r", meshdisc, 1, degree=1)
+
+
+
+
+
+
+
radius_fn = sympy.sqrt(
+    meshdisc.rvec.dot(meshdisc.rvec)
+)  # normalise by outer radius if not 1.0
+unit_rvec = meshdisc.X / (radius_fn)
+gravity_fn = radius_fn
+
+# Some useful coordinate stuff
+
+x, y = meshdisc.CoordinateSystem.X
+ra, th = meshdisc.CoordinateSystem.xR
+
+hw = 1000.0 / res
+surface_fn_a = sympy.exp(-(((ra - r_o) / r_o) ** 2) * hw)
+surface_fn = sympy.exp(-(((meshr.sym[0] - r_o) / r_o) ** 2) * hw)
+
+base_fn_a = sympy.exp(-(((ra - r_i) / r_o) ** 2) * hw)
+base_fn = sympy.exp(-(((meshr.sym[0] - r_i) / r_o) ** 2) * hw)
+
+free_slip_penalty_upper = v_soln.sym.dot(unit_rvec) * unit_rvec * surface_fn
+free_slip_penalty_lower = v_soln.sym.dot(unit_rvec) * unit_rvec * base_fn
+
+
+
+
+
+
+
# Create Stokes object
+
+stokes = Stokes(
+    meshdisc,
+    velocityField=v_soln,
+    pressureField=p_soln,
+    solver_name="stokes",
+)
+
+# Set solve options here (or remove default values
+# stokes.petsc_options.getAll()
+# stokes.petsc_options.delValue("ksp_monitor")
+# stokes.petsc_options["snes_test_jacobian"] = None
+
+# T dependent visc
+delta_eta = 10**log10_delta_eta
+
+stokes.petsc_options["snes_rtol"] = 1 / delta_eta
+
+viscosity = delta_eta * sympy.exp(-sympy.log(delta_eta) * t_soln.sym[0])
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.viscosity = viscosity
+stokes.penalty = 0.0
+
+stokes.saddle_preconditioner = 1.0 / viscosity
+
+# Velocity boundary conditions
+stokes.add_dirichlet_bc((0.0, 0.0), "Upper", (0, 1))
+# stokes.add_dirichlet_bc((0.0,0.0), "Lower", (0,1))
+
+# Buoyancy force RHS plus free slip surface enforcement
+buoyancy_force = Rayleigh * t_soln.sym[0] * unit_rvec * (1.0 - base_fn)
+penalty_terms = 10000000 * free_slip_penalty_lower
+
+stokes.bodyforce = buoyancy_force - penalty_terms
+
+
+
+
+
+
+
# Create adv_diff object
+
+# Set some things
+k = 1.0
+h = 0.0
+
+adv_diff = uw.systems.AdvDiffusionSLCN(
+    meshdisc,
+    u_Field=t_soln,
+    V_fn=v_soln,
+    solver_name="adv_diff",
+)
+
+adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel
+adv_diff.constitutive_model.Parameters.diffusivity = k
+adv_diff.theta = 0.5
+
+
+
+
+
+
+
# Define T boundary conditions via a sympy function
+
+import sympy
+
+init_t = 0.9 + 0.05 * (sympy.cos(sympy.pi * th / 2)) * sympy.cos(
+    0.5 * np.pi * (ra - r_i) / (r_o - r_i)
+)
+
+adv_diff.add_dirichlet_bc(1.0, "Lower")
+adv_diff.add_dirichlet_bc(0.0, "Upper")
+
+
+
+
+
+
+
with meshdisc.access(t_soln):
+    t_soln.data[...] = uw.function.evaluate(init_t, t_soln.coords, meshdisc.N).reshape(
+        -1, 1
+    )
+
+with meshdisc.access(meshr):
+    meshr.data[:, 0] = uw.function.evaluate(
+        sympy.sqrt(x**2 + y**2), meshdisc.data, meshdisc.N
+    )  # cf radius_fn which is 0->1
+
+
+
+
+
+
+
# check the stokes solve is set up and that it converges
+stokes.solve(zero_init_guess=True)
+
+
+
+
+
+
+
# Check the diffusion part of the solve converges
+adv_diff.solve(timestep=0.1 * stokes.estimate_dt())
+
+
+
+
+
+
+
# adv_diff
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+
+if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh =  vis.mesh_to_pv_mesh(meshdisc)
+    pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+
+    points = vis.meshVariable_to_pv_cloud(t_soln)
+    points.point_data["Tp"] = vis.scalar_fn_to_pv_points(points, t_soln.sym)
+    point_cloud = pv.PolyData(points)
+
+    # point sources at cell centres
+    subsample = 2
+    cpoints = np.zeros((meshdisc._centroids[::subsample, 0].shape[0], 3))
+    cpoints[:, 0] = meshdisc._centroids[::subsample, 0]
+    cpoints[:, 1] = meshdisc._centroids[::subsample, 1]
+    cpoint_cloud = pv.PolyData(cpoints)
+
+    pvstream = pvmesh.streamlines_from_source(
+                                                cpoint_cloud,
+                                                vectors="V",
+                                                integrator_type=2,
+                                                integration_direction="forward",
+                                                compute_vorticity=False,
+                                                max_steps=100,
+                                                surface_streamlines=True,
+                                            )
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_mesh(pvmesh, "Gray", "wireframe")
+
+    # pl.add_mesh(
+    #     pvmesh, cmap="coolwarm", edge_color="Black",
+    #     show_edges=True, scalars="T", use_transparency=False, opacity=0.5,
+    # )
+
+    pl.add_points(
+        point_cloud,
+        cmap="coolwarm",
+        render_points_as_spheres=False,
+        point_size=3,
+        opacity=0.33,
+    )
+
+    pl.add_mesh(pvstream, opacity=0.5)
+    pl.add_arrows(pvmesh.points, pvmesh.point_data["V"], mag=1.0e-4)
+
+    # pl.add_points(pdata)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
def plot_T_mesh(filename):
+    if uw.mpi.size == 1:
+        
+        import pyvista as pv
+        import underworld3.visualisation as vis
+
+        pvmesh = vis.mesh_to_pv_mesh(meshdisc)
+        pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)
+        pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+
+        # point sources at cell centres
+        cpoints = np.zeros((meshdisc._centroids.shape[0], 3))
+        cpoints[:, 0] = meshdisc._centroids[:, 0]
+        cpoints[:, 1] = meshdisc._centroids[:, 1]
+        cpoint_cloud = pv.PolyData(cpoints)
+
+        pvstream = pvmesh.streamlines_from_source(
+                                                    cpoint_cloud,
+                                                    vectors="V",
+                                                    integrator_type=45,
+                                                    integration_direction="forward",
+                                                    compute_vorticity=False,
+                                                    max_steps=100,
+                                                    surface_streamlines=True,
+                                                )
+
+
+        points = vis.meshVariable_to_pv_cloud(t_soln)
+        points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym)
+        point_cloud = pv.PolyData(points)
+        
+        pl = pv.Plotter(window_size=(750, 750))
+
+        pl.add_mesh(
+            pvmesh,
+            cmap="coolwarm",
+            edge_color="Gray",
+            show_edges=True,
+            scalars="T",
+            use_transparency=False,
+            opacity=0.5,
+        )
+
+        pl.add_points(
+            point_cloud,
+            cmap="coolwarm",
+            render_points_as_spheres=False,
+            point_size=10,
+            opacity=0.5,
+        )
+
+        pl.add_mesh(pvstream, opacity=0.4)
+
+        pl.remove_scalar_bar("T")
+        pl.remove_scalar_bar("V")
+
+        pl.screenshot(
+            filename="{}.png".format(filename),
+            window_size=(1280, 1280),
+            return_img=False,
+        )
+        # pl.show()
+        pl.close()
+
+        pvmesh.clear_data()
+        pvmesh.clear_point_data()
+
+        pv.close_all()
+
+
+
+
+
+
+
t_step = 0
+
+
+
+
+
+
+
# Convection model / update in time
+
+##
+## There is a strange interaction here between the solvers if the zero_guess is
+## set to False
+##
+
+expt_name = f"output/Ra1e6_cyl_eta1e{log10_delta_eta}"
+
+for step in range(0, 1000):
+    stokes.solve(zero_init_guess=True)
+    delta_t = 2.0 * stokes.estimate_dt()
+    adv_diff.solve(timestep=delta_t, zero_init_guess=True)
+
+    # stats then loop
+    tstats = t_soln.stats()
+
+    if uw.mpi.rank == 0:
+        print("Timestep {}, dt {}".format(step, delta_t))
+    #         print(tstats)
+
+    if t_step % 5 == 0:
+        plot_T_mesh(filename="{}_step_{}".format(expt_name, t_step))
+
+    t_step += 1
+
+# savefile = "{}_ts_{}.h5".format(expt_name,step)
+# meshdisc.save(savefile)
+# v_soln.save(savefile)
+# t_soln.save(savefile)
+# meshdisc.generate_xdmf(savefile)
+
+
+
+
+

savefile = “output_conv/convection_cylinder.h5”.format(step) +meshdisc.save(savefile) +v_soln.save(savefile) +t_soln.save(savefile) +meshdisc.generate_xdmf(savefile)

+
+
+
if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshdisc)
+
+    velocity_points = vis.meshVariable_to_pv_cloud(stokes.u)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym)
+
+    points = vis.meshVariable_to_pv_cloud(t_soln)
+    points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym)
+    point_cloud = pv.PolyData(points)
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=1e-4, opacity=0.75)
+    # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1)
+
+    pl.add_points(
+        point_cloud,
+        cmap="coolwarm",
+        render_points_as_spheres=True,
+        point_size=7,
+        opacity=0.25,
+    )
+
+    pl.add_mesh(pvmesh, "Black", "wireframe", opacity=0.75)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Convection/Ex_Convection_4_SLCN_Cartesian-NL.html b/main/Notebooks/Examples-Convection/Ex_Convection_4_SLCN_Cartesian-NL.html new file mode 100644 index 0000000..7f62d34 --- /dev/null +++ b/main/Notebooks/Examples-Convection/Ex_Convection_4_SLCN_Cartesian-NL.html @@ -0,0 +1,977 @@ + + + + + + + + + + + Non-linear viscosity convection, Cartesian domain (benchmark) — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Non-linear viscosity convection, Cartesian domain (benchmark)

+ +
+
+ +
+
+
+ + + + +
+ +
+

Non-linear viscosity convection, Cartesian domain (benchmark)#

+

This is a simple example in which we try to instantiate two solvers on the mesh and have them use a common set of variables.

+

We set up a v, p, T system in which we will solve for a steady-state T field in response to thermal boundary conditions and then use the steady-state T field to compute a stokes flow in response.

+

The next step is to add particles at node points and sample back along the streamlines to find values of the T field at a previous time.

+

(Note, we keep all the pieces from previous increments of this problem to ensure that we don’t break something along the way)

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+
+import underworld3 as uw
+from underworld3.systems import Stokes
+from underworld3 import function
+
+import numpy as np
+import sympy
+
+
+
+
+
+
+
meshbox = uw.meshing.UnstructuredSimplexBox(
+    minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0), cellSize=1.0 / 24.0, qdegree=3
+)
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable("U", meshbox, meshbox.dim, degree=2)
+p_soln = uw.discretisation.MeshVariable("P", meshbox, 1, degree=1)
+t_soln = uw.discretisation.MeshVariable("T", meshbox, 1, degree=3)
+t_0 = uw.discretisation.MeshVariable("T0", meshbox, 1, degree=3)
+
+visc = uw.discretisation.MeshVariable(r"\eta(\dot\varepsilon)", meshbox, 1, degree=1)
+
+
+
+
+
+
+
# Create Stokes object
+
+stokes = Stokes(
+    meshbox,
+    velocityField=v_soln,
+    pressureField=p_soln,
+    solver_name="stokes",
+)
+
+# Set solve options here (or remove default values
+# stokes.petsc_options.getAll()
+# stokes.petsc_options.delValue("ksp_monitor")
+
+# Linear visc
+delta_eta = 1.0e6
+
+viscosity_L = delta_eta * sympy.exp(-sympy.log(delta_eta) * t_soln.sym[0])
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.viscosity = viscosity_L
+
+stokes.saddle_preconditioner = 1 / stokes.constitutive_model.Parameters.viscosity
+stokes.penalty = 0.0
+
+# Velocity boundary conditions
+stokes.add_dirichlet_bc((0.0,), "Left", (0,))
+stokes.add_dirichlet_bc((0.0,), "Right", (0,))
+stokes.add_dirichlet_bc((0.0,), "Top", (1,))
+stokes.add_dirichlet_bc((0.0,), "Bottom", (1,))
+
+
+
+
+
+
+
stokes.constitutive_model.solver = stokes
+
+
+
+
+
+
+
# isinstance(Stokes, uw.systems.SNES_SaddlePoint)
+
+
+
+
+
+
+
# Create a density structure / buoyancy force
+# gravity will vary linearly from zero at the centre
+# of the sphere to (say) 1 at the surface
+
+import sympy
+
+# Some useful coordinate stuff
+
+x, y = meshbox.X
+
+
+
+
+
+
+
# Create adv_diff object
+
+# Set some things
+k = 1.0
+h = 0.0
+
+adv_diff = uw.systems.AdvDiffusion(
+    meshbox,
+    u_Field=t_soln,
+    V_fn=v_soln,
+    solver_name="adv_diff",
+)
+
+adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel
+adv_diff.constitutive_model.Parameters.diffusivity = k
+
+adv_diff.theta = 0.5
+
+
+
+
+
+
+
# Create a scalar function calculator that we can use to obtain viscosity etc
+
+scalar_projection = uw.systems.Projection(meshbox, visc)
+scalar_projection.uw_function = 0.1 + 10.0 / (1.0 + stokes.Unknowns.Einv2)
+scalar_projection.smoothing = 1.0e-6
+
+
+
+
+
+
+
expt_name = "output/Ra1e6_NL"
+
+Rayleigh = 1.0e6
+buoyancy_force = Rayleigh * t_soln.sym[0]
+stokes.bodyforce = sympy.Matrix([0, buoyancy_force])
+
+
+
+
+
+
+
# Define T boundary conditions via a sympy function
+
+import sympy
+
+init_t = 0.9 * (0.05 * sympy.cos(sympy.pi * x) + sympy.cos(0.5 * np.pi * y)) + 0.05
+
+adv_diff.add_dirichlet_bc(1.0, "Bottom")
+adv_diff.add_dirichlet_bc(0.0, "Top")
+
+with meshbox.access(t_0, t_soln):
+    t_0.data[...] = uw.function.evaluate(init_t, t_0.coords).reshape(-1, 1)
+    t_soln.data[...] = t_0.data[...]
+
+
+
+
+
+
+
# Linear problem for initial solution of velocity field
+stokes.solve()
+
+
+
+
+
+
+
# Now make the viscosity non-linear
+
+viscosity_NL = viscosity_L * (0.1 + 10.0 / (1.0 + stokes.Unknowns.Einv2))
+
+stokes.constitutive_model.Parameters.viscosity = viscosity_NL
+stokes.saddle_preconditioner = 1 / viscosity_NL
+
+
+
+
+
+
+
stokes.saddle_preconditioner
+
+
+
+
+
+
+
stokes.solve(zero_init_guess=False)
+
+
+
+
+
+
+
# Check the diffusion part of the solve converges
+adv_diff.solve(timestep=0.01 * stokes.estimate_dt())
+
+
+
+
+
+
+
# Compute viscosity field
+scalar_projection.solve()
+
+
+
+
+
+
+
with meshbox.access():
+    print(visc.min(), visc.max())
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshbox)
+    pvmesh.point_data["V"] = 10.0 * vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) / vis.vector_fn_to_pv_points(pvmesh, v_soln.sym).max()
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+    pvmesh.point_data["eta"] = vis.scalar_fn_to_pv_points(pvmesh, visc.sym)
+
+    # point sources at cell centres
+    cpoints = np.zeros((meshbox._centroids[::2].shape[0], 3))
+    cpoints[:, 0] = meshbox._centroids[::2, 0]
+    cpoints[:, 1] = meshbox._centroids[::2, 1]
+    cpoint_cloud = pv.PolyData(cpoints)
+
+    pvstream = pvmesh.streamlines_from_source(
+                                                cpoint_cloud,
+                                                vectors="V",
+                                                integrator_type=45,
+                                                integration_direction="forward",
+                                                compute_vorticity=False,
+                                                max_steps=25,
+                                                surface_streamlines=True,
+                                            )
+
+    points = vis.meshVariable_to_pv_cloud(t_soln)
+    points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym)
+    point_cloud = pv.PolyData(points)
+
+    ## PLOTTING
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Gray",
+        show_edges=True,
+        scalars="T",
+        use_transparency=False,
+        opacity=0.5,
+    )
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="Greys",
+        show_edges=False,
+        scalars="eta",
+        use_transparency=False,
+        opacity=0.25,
+    )
+
+    # pl.add_points(point_cloud, cmap="coolwarm", render_points_as_spheres=False, point_size=10, opacity=0.5)
+
+    pl.add_mesh(pvstream, opacity=0.4)
+
+    pl.remove_scalar_bar("T")
+    pl.remove_scalar_bar("V")
+    pl.remove_scalar_bar("eta")
+
+    # pl.screenshot(filename="{}.png".format(filename), window_size=(1280, 1280), return_img=False)
+    pl.show()
+
+
+
+
+
+
+
def plot_T_mesh(filename):
+    if uw.mpi.size == 1:
+        import pyvista as pv
+        import underworld3.visualisation as vis
+    
+        pvmesh = vis.mesh_to_pv_mesh(meshbox)
+        pvmesh.point_data["V"] = 10.0 * vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) / vis.vector_fn_to_pv_points(pvmesh, v_soln.sym).max()
+        pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+        pvmesh.point_data["eta"] = vis.scalar_fn_to_pv_points(pvmesh, visc.sym)
+
+        # point sources at cell centres
+        cpoints = np.zeros((meshbox._centroids[::2].shape[0], 3))
+        cpoints[:, 0] = meshbox._centroids[::2, 0]
+        cpoints[:, 1] = meshbox._centroids[::2, 1]
+        cpoint_cloud = pv.PolyData(cpoints)
+
+        pvstream = pvmesh.streamlines_from_source(
+                                                    cpoint_cloud,
+                                                    vectors="V",
+                                                    integrator_type=45,
+                                                    integration_direction="forward",
+                                                    compute_vorticity=False,
+                                                    max_steps=25,
+                                                    surface_streamlines=True,
+                                                )
+
+        points = vis.meshVariable_to_pv_cloud(t_soln)
+        points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym)
+        point_cloud = pv.PolyData(points)
+        
+        ## PLOTTING
+        pl = pv.Plotter(window_size=(750, 750))
+
+        pl.add_mesh(
+            pvmesh,
+            cmap="coolwarm",
+            edge_color="Gray",
+            show_edges=True,
+            scalars="T",
+            use_transparency=False,
+            opacity=0.5,
+        )
+
+        pl.add_mesh(
+            pvmesh,
+            cmap="Greys",
+            show_edges=False,
+            scalars="eta",
+            use_transparency=False,
+            opacity=0.25,
+        )
+
+        # pl.add_points(point_cloud, cmap="coolwarm", render_points_as_spheres=False, point_size=10, opacity=0.5)
+
+        pl.add_mesh(pvstream, opacity=0.4)
+
+        pl.remove_scalar_bar("T")
+        pl.remove_scalar_bar("V")
+        pl.remove_scalar_bar("eta")
+
+        pl.screenshot(
+            filename="{}.png".format(filename),
+            window_size=(1280, 1280),
+            return_img=False,
+        )
+
+
+
+
+
+
+
# Convection model / update in time
+
+
+for step in range(0, 250):
+    stokes.solve(zero_init_guess=False)
+    delta_t = 5.0 * stokes.estimate_dt()
+    adv_diff.solve(timestep=delta_t, zero_init_guess=False)
+
+    # stats then loop
+    tstats = t_soln.stats()
+
+    if uw.mpi.rank == 0:
+        print("Timestep {}, dt {}".format(step, delta_t))
+
+    plot_T_mesh(filename="{}_step_{}".format(expt_name, step))
+
+    # savefile = "{}_ts_{}.h5".format(expt_name,step)
+    # meshbox.save(savefile)
+    # v_soln.save(savefile)
+    # t_soln.save(savefile)
+    # meshbox.generate_xdmf(savefile)
+
+pass
+
+
+
+
+

savefile = “output_conv/convection_cylinder.h5”.format(step) +meshbox.save(savefile) +v_soln.save(savefile) +t_soln.save(savefile) +meshbox.generate_xdmf(savefile)

+
+
+
if uw.mpi.size == 1:
+
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshbox)
+    velocity_points = vis.meshVariable_to_pv_cloud(stokes.u)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym)
+
+    points = vis.meshVariable_to_pv_cloud(t_soln)
+    points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym)
+    point_cloud = pv.PolyData(points)
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.00002, opacity=0.75)
+    
+    pl.add_points(
+        point_cloud,
+        cmap="coolwarm",
+        render_points_as_spheres=True,
+        point_size=7.5,
+        opacity=0.25,
+    )
+
+    pl.add_mesh(pvmesh, "Black", "wireframe", opacity=0.75)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Convection/Ex_Convection_5_SLCN_Cartesian-Yield.html b/main/Notebooks/Examples-Convection/Ex_Convection_5_SLCN_Cartesian-Yield.html new file mode 100644 index 0000000..a19ea73 --- /dev/null +++ b/main/Notebooks/Examples-Convection/Ex_Convection_5_SLCN_Cartesian-Yield.html @@ -0,0 +1,977 @@ + + + + + + + + + + + Non-linear viscosity convection, Cartesian domain (benchmark) — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Non-linear viscosity convection, Cartesian domain (benchmark)

+ +
+
+ +
+
+
+ + + + +
+ +
+

Non-linear viscosity convection, Cartesian domain (benchmark)#

+

This is a convection example with a yield stress but no strain softening. This can be one of the more challenging problems from a solver point of view because the structure of the non-linear response does not get locked into the solution by localisation.

+

This example demonstrates that the sympy.Piecewise description of the viscosity upon yielding is differentiable and can be used to construct Jacobians.

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+
+import underworld3 as uw
+from underworld3.systems import Stokes
+from underworld3 import function
+
+import numpy as np
+import sympy
+
+
+
+
+
+
+
meshbox = uw.meshing.UnstructuredSimplexBox(
+    minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0), cellSize=1.0 / 24.0, qdegree=3
+)
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable("U", meshbox, meshbox.dim, degree=2)
+p_soln = uw.discretisation.MeshVariable("P", meshbox, 1, degree=1)
+t_soln = uw.discretisation.MeshVariable("T", meshbox, 1, degree=3)
+t_0 = uw.discretisation.MeshVariable("T0", meshbox, 1, degree=3)
+
+visc = uw.discretisation.MeshVariable(r"\eta(\dot\varepsilon)", meshbox, 1, degree=2)
+tau_inv = uw.discretisation.MeshVariable(r"|\tau|", meshbox, 1, degree=2)
+
+
+
+
+
+
+
# Create Stokes object
+
+stokes = Stokes(
+    meshbox,
+    velocityField=v_soln,
+    pressureField=p_soln,
+    solver_name="stokes",
+)
+
+# Set solve options here (or remove default values
+# stokes.petsc_options.getAll()
+# stokes.petsc_options.delValue("ksp_monitor")
+stokes.petsc_options["ksp_monitor"] = None
+
+# Linear visc
+delta_eta = 1.0e6
+
+viscosity_L = delta_eta * sympy.exp(-sympy.log(delta_eta) * t_soln.sym[0])
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.viscosity = viscosity_L
+stokes.saddle_preconditioner = 1 / viscosity_L
+stokes.penalty = 0.0
+
+# Velocity boundary conditions
+stokes.add_dirichlet_bc((0.0,), "Left", (0,))
+stokes.add_dirichlet_bc((0.0,), "Right", (0,))
+stokes.add_dirichlet_bc((0.0,), "Top", (1,))
+stokes.add_dirichlet_bc((0.0,), "Bottom", (1,))
+
+
+
+
+
+
+
# Create a density structure / buoyancy force
+# gravity will vary linearly from zero at the centre
+# of the sphere to (say) 1 at the surface
+
+import sympy
+
+# Some useful coordinate stuff
+
+x, y = meshbox.X
+
+
+
+
+
+
+
# Create adv_diff object
+
+# Set some things
+k = 1.0
+h = 0.0
+
+adv_diff = uw.systems.AdvDiffusion(
+    meshbox,
+    u_Field=t_soln,
+    V_fn=v_soln,
+    solver_name="adv_diff",
+)
+
+adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel
+adv_diff.constitutive_model.Parameters.diffusivity = k
+
+adv_diff.theta = 0.5
+
+
+
+
+
+
+
# Create scalar function evaluators that we can use to obtain viscosity / stress
+
+viscosity_evaluation = uw.systems.Projection(meshbox, visc)
+viscosity_evaluation.uw_function = 0.1 + 10.0 / (
+    1.0 + stokes.Unknowns.Einv2
+)  # stokes.constitutive_model.material_properties.viscosity
+viscosity_evaluation.smoothing = 1.0e-3
+#
+stress_inv_evaluation = uw.systems.Projection(meshbox, tau_inv)
+stress_inv_evaluation.uw_function = (
+    2.0 * stokes.constitutive_model.Parameters.viscosity * stokes.Unknowns.Einv2
+)
+stress_inv_evaluation.smoothing = 1.0e-3
+
+
+
+
+
+
+
expt_name = "output/Ra1e6_TauY"
+
+Rayleigh = 1.0e6
+buoyancy_force = Rayleigh * t_soln.sym[0]
+stokes.bodyforce = sympy.Matrix([0, buoyancy_force])
+
+
+
+
+
+
+
# Define T boundary conditions via a sympy function
+
+import sympy
+
+init_t = 0.9 * (0.05 * sympy.cos(sympy.pi * x) + sympy.cos(0.5 * np.pi * y)) + 0.05
+
+adv_diff.add_dirichlet_bc(1.0, "Bottom")
+adv_diff.add_dirichlet_bc(0.0, "Top")
+
+with meshbox.access(t_0, t_soln):
+    t_0.data[...] = uw.function.evaluate(init_t, t_0.coords).reshape(-1, 1)
+    t_soln.data[...] = t_0.data[...]
+
+
+
+
+
+
+
# Linear problem for initial solution of velocity field
+stokes.solve()
+
+
+
+
+
+
+
# Now make the viscosity non-linear
+
+tau_Y = 1.0e5 * (1 + 100 * (1 - y))
+
+viscosity_NL = sympy.Piecewise(
+    (viscosity_L, 2 * viscosity_L * stokes.Unknowns.Einv2 < tau_Y),
+    (tau_Y / (2 * stokes.Unknowns.Einv2), True),
+)
+
+stokes.constitutive_model.Parameters.viscosity = viscosity_NL
+stokes.saddle_preconditioner = 1 / viscosity_NL
+
+
+
+
+
+
+
stokes.solve(zero_init_guess=False)
+
+
+
+
+
+
+
# Check the diffusion part of the solve converges
+adv_diff.solve(timestep=0.01 * stokes.estimate_dt())
+
+
+
+
+
+
+
# Compute viscosity field
+viscosity_evaluation.solve()
+stress_inv_evaluation.solve()
+
+
+
+
+
+
+
with meshbox.access():
+    print(visc.min(), visc.max())
+    print(tau_inv.min(), tau_inv.max())
+
+
+
+
+
+
+
if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshbox)
+    pvmesh.point_data["V"] = 1.0 * vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) / vis.vector_fn_to_pv_points(pvmesh, v_soln.sym).max()
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+    pvmesh.point_data["eta"] = vis.scalar_fn_to_pv_points(pvmesh, visc.sym)
+    pvmesh.point_data["tau"] = vis.scalar_fn_to_pv_points(pvmesh, tau_inv.sym)
+
+    # point sources at cell centres
+    subsample = 10
+    cpoints = np.zeros((meshbox._centroids[::subsample].shape[0], 3))
+    cpoints[:, 0] = meshbox._centroids[::subsample, 0]
+    cpoints[:, 1] = meshbox._centroids[::subsample, 1]
+    cpoint_cloud = pv.PolyData(cpoints)
+
+    pvstream = pvmesh.streamlines_from_source(
+                                                cpoint_cloud,
+                                                vectors="V",
+                                                integrator_type=45,
+                                                integration_direction="forward",
+                                                compute_vorticity=False,
+                                                max_steps=25,
+                                                surface_streamlines=True,
+                                            )
+
+    points = vis.meshVariable_to_pv_cloud(t_soln)
+    points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym)
+    point_cloud = pv.PolyData(points)
+
+    ## PLOTTING
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Gray",
+        show_edges=False,
+        scalars="T",
+        use_transparency=False,
+        opacity=0.75,
+    )
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="Greys",
+        show_edges=False,
+        scalars="eta",
+        use_transparency=False,
+        opacity="geom",
+    )
+
+    # pl.add_points(point_cloud, cmap="coolwarm", render_points_as_spheres=False, point_size=10, opacity=0.5)
+
+    pl.add_mesh(pvstream, opacity=0.2)
+
+    # pl.screenshot(filename="{}.png".format(filename), window_size=(1280, 1280), return_img=False)
+    pl.show()
+
+
+
+
+
+
+
def plot_T_mesh(filename):
+    if uw.mpi.size == 1:
+        import pyvista as pv
+        import underworld3.visualisation as vis
+
+        pvmesh = vis.mesh_to_pv_mesh(meshbox)
+        pvmesh.point_data["V"] = 10.0 * vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) / vis.vector_fn_to_pv_points(pvmesh, v_soln.sym).max()
+        pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+        pvmesh.point_data["eta"] = vis.scalar_fn_to_pv_points(pvmesh, visc.sym)
+        pvmesh.point_data["tau"] = vis.scalar_fn_to_pv_points(pvmesh, tau_inv.sym)
+
+        # point sources at cell centres
+        subsample = 10
+        cpoints = np.zeros((meshbox._centroids[::subsample].shape[0], 3))
+        cpoints[:, 0] = meshbox._centroids[::subsample, 0]
+        cpoints[:, 1] = meshbox._centroids[::subsample, 1]
+        cpoint_cloud = pv.PolyData(cpoints)
+
+        pvstream = pvmesh.streamlines_from_source(
+                                                    cpoint_cloud,
+                                                    vectors="V",
+                                                    integrator_type=45,
+                                                    integration_direction="forward",
+                                                    compute_vorticity=False,
+                                                    max_steps=25,
+                                                    surface_streamlines=True,
+                                                )
+
+        points = vis.meshVariable_to_pv_cloud(t_soln)
+        points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym)
+        point_cloud = pv.PolyData(points)
+    
+        ## PLOTTING
+    
+        pl = pv.Plotter(window_size=(750, 750))
+
+        pl.add_mesh(
+            pvmesh,
+            cmap="coolwarm",
+            edge_color="Gray",
+            show_edges=True,
+            scalars="T",
+            use_transparency=False,
+            opacity=0.75,
+        )
+
+        pl.add_mesh(
+            pvmesh,
+            cmap="Greys",
+            show_edges=False,
+            scalars="eta",
+            use_transparency=False,
+            opacity="geom",
+        )
+
+        pl.add_mesh(pvstream, opacity=0.5)
+
+        for key in pvmesh.point_data.keys():
+            try:
+                pl.remove_scalar_bar(key)
+            except KeyError:
+                pass
+
+        pl.screenshot(
+            filename="{}.png".format(filename),
+            window_size=(1280, 1280),
+            return_img=False,
+        )
+
+
+
+
+
+
+
# Convection model / update in time
+
+
+for step in range(0, 250): 
+    stokes.solve(zero_init_guess=False)
+    delta_t = 5.0 * stokes.estimate_dt()
+    adv_diff.solve(timestep=delta_t, zero_init_guess=False)
+
+    # stats then loop
+    tstats = t_soln.stats()
+
+    if uw.mpi.rank == 0:
+        print("Timestep {}, dt {}".format(step, delta_t))
+
+    plot_T_mesh(filename="{}_step_{}".format(expt_name, step))
+
+    # savefile = "{}_ts_{}.h5".format(expt_name,step)
+    # meshbox.save(savefile)
+    # v_soln.save(savefile)
+    # t_soln.save(savefile)
+    # meshbox.generate_xdmf(savefile)
+
+pass
+
+
+
+
+

savefile = “output_conv/convection_cylinder.h5”.format(step) +meshbox.save(savefile) +v_soln.save(savefile) +t_soln.save(savefile) +meshbox.generate_xdmf(savefile)

+
+
+
if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshbox)
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+    velocity_points = vis.meshVariable_to_pv_cloud(stokes.u)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym)
+
+    points = vis.meshVariable_to_pv_cloud(t_soln)
+    points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym)
+    point_cloud = pv.PolyData(points)
+
+    points = np.zeros((t_soln.coords.shape[0], 3))
+    points[:, 0] = t_soln.coords[:, 0]
+    points[:, 1] = t_soln.coords[:, 1]
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.00002, opacity=0.75)
+    # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1)
+
+    pl.add_points(
+        point_cloud,
+        cmap="coolwarm",
+        render_points_as_spheres=True,
+        point_size=7.5,
+        opacity=0.25,
+    )
+
+    pl.add_mesh(pvmesh, "Black", "wireframe", opacity=0.75)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Convection/Ex_Convection_Cartesian-Swarm.html b/main/Notebooks/Examples-Convection/Ex_Convection_Cartesian-Swarm.html new file mode 100644 index 0000000..a3e84aa --- /dev/null +++ b/main/Notebooks/Examples-Convection/Ex_Convection_Cartesian-Swarm.html @@ -0,0 +1,900 @@ + + + + + + + + + + + Constant viscosity convection, Cartesian domain (benchmark) — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Constant viscosity convection, Cartesian domain (benchmark)

+ +
+
+ +
+
+
+ + + + +
+ +
+

Constant viscosity convection, Cartesian domain (benchmark)#

+

This is a simple example in which we try to instantiate two solvers on the mesh and have them use a common set of variables.

+

We set up a v, p, T system in which we will solve for a steady-state T field in response to thermal boundary conditions and then use the steady-state T field to compute a stokes flow in response.

+

The next step is to add particles at node points and sample back along the streamlines to find values of the T field at a previous time.

+

(Note, we keep all the pieces from previous increments of this problem to ensure that we don’t break something along the way)

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+
+import underworld3 as uw
+from underworld3.systems import Stokes
+from underworld3 import function
+
+import numpy as np
+import sympy
+
+
+
+
+
+
+
meshbox = uw.meshing.UnstructuredSimplexBox(
+                                            minCoords=(0.0, 0.0),
+                                            maxCoords=(1.0, 1.0),
+                                            cellSize=1.0 / 32.0,
+                                            regular=True,
+                                            )
+meshbox.dm.view()
+
+
+
+
+
+
+

+import sympy
+
+# Some useful coordinate stuff
+
+x = meshbox.N.x
+y = meshbox.N.y
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshbox)
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    # pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.5)
+    pl.add_mesh(pvmesh, edge_color="Black", show_edges=True)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable("U", meshbox, meshbox.dim, degree=2)
+p_soln = uw.discretisation.MeshVariable("P", meshbox, 1, degree=1)
+t_soln = uw.discretisation.MeshVariable("T", meshbox, 1, degree=3)
+t_0 = uw.discretisation.MeshVariable("T0", meshbox, 1, degree=3)
+
+
+
+
+
+
+
swarm = uw.swarm.Swarm(mesh=meshbox)
+T1 = uw.swarm.SwarmVariable("Tminus1", swarm, 1, proxy_degree=3)
+swarm.populate(fill_param=5)
+
+
+
+
+
+
+
ad = uw.systems.AdvDiffusion(meshbox, t_soln, T1.sym, order=3)
+
+ad._u_star_projector.smoothing = 0.0
+
+ad.add_dirichlet_bc(1.0, "Bottom")
+ad.add_dirichlet_bc(0.0, "Top")
+
+init_t = 0.01 * sympy.sin(5.0 * x) * sympy.sin(np.pi * y) + (1.0 - y)
+
+with meshbox.access(t_0, t_soln):
+    t_0.data[...] = uw.function.evaluate(init_t, t_0.coords).reshape(-1, 1)
+    t_soln.data[...] = t_0.data[...]
+
+with swarm.access(T1):
+    T1.data[...] = uw.function.evaluate(
+        init_t, swarm.particle_coordinates.data
+    ).reshape(-1, 1)
+
+
+
+
+
+
+
# Create Stokes object
+
+stokes = Stokes(
+                meshbox,
+                velocityField=v_soln,
+                pressureField=p_soln,
+                u_degree=v_soln.degree,
+                p_degree=p_soln.degree,
+                solver_name="stokes",
+                verbose=False,
+            )
+
+# Set solve options here (or remove default values
+# stokes.petsc_options.getAll()
+stokes.petsc_options.delValue("ksp_monitor")
+
+# Constant visc
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel(meshbox.dim)
+stokes.constitutive_model.Parameters.viscosity = 1
+
+# Velocity boundary conditions
+stokes.add_dirichlet_bc((0.0,), "Left", (0,))
+stokes.add_dirichlet_bc((0.0,), "Right", (0,))
+stokes.add_dirichlet_bc((0.0,), "Top", (1,))
+stokes.add_dirichlet_bc((0.0,), "Bottom", (1,))
+
+
+
+
+
+
+
buoyancy_force = 1.0e6 * t_soln.fn
+stokes.bodyforce = meshbox.N.j * buoyancy_force
+
+# check the stokes solve is set up and that it converges
+stokes.solve()
+
+
+
+
+
+
+
# check the projection
+if uw.mpi.size == 1 and ad.projection:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshbox)
+    pvmesh.point_data["T1"] = vis.scalar_fn_to_pv_points(pvmesh, T1.sym)
+    pvmesh.point_data["mT1"] = vis.scalar_fn_to_pv_points(pvmesh, ad._u_star_projected.sym)
+    pvmesh.point_data["dT1"] = vis.scalar_fn_to_pv_points(pvmesh, T1.sym) - vis.scalar_fn_to_pv_points(pvmesh, ad._u_star_projected.sym)
+
+    velocity_points = vis.meshVariable_to_pv_cloud(stokes.u)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym)
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    # pl.add_mesh(pvmesh,'Black', 'wireframe')
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        scalars="dT1",
+        use_transparency=False,
+        opacity=0.5,
+    )
+
+    # pl.add_arrows(arrow_loc, arrow_length, mag=1.0e-4, opacity=0.5)
+    # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1)
+
+    # pl.add_points(pdata)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
def plot_T_mesh(filename):
+    if uw.mpi.size == 1:
+        
+        import pyvista as pv
+        import underworld3.visualisation as vis
+
+        pvmesh = vis.mesh_to_pv_mesh(meshbox)
+        pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+        tpoints = vis.meshVariable_to_pv_cloud(t_soln)
+        tpoints.point_data["T"] = vis.scalar_fn_to_pv_points(tpoints, t_soln.sym)
+        tpoint_cloud = pv.PolyData(tpoints)
+
+        spoints = vis.swarm_to_pv_cloud(swarm)
+        swarm_point_cloud = pv.PolyData(spoints)
+        with swarm.access():
+            swarm_point_cloud.point_data["T1"] = T1.data.copy()
+
+        velocity_points = vis.meshVariable_to_pv_cloud(stokes.u)
+        velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym)
+
+        pl = pv.Plotter(window_size=(750, 750))
+
+        pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.00001, opacity=0.75)
+
+        pl.add_points(
+            swarm_point_cloud,  # cmap="RdYlBu_r", scalars="T1",
+            color="Black",
+            render_points_as_spheres=True,
+            clim=[0.0, 1.0],
+            point_size=1.0,
+            opacity=0.5,
+        )
+
+        pl.add_points(
+            point_cloud,
+            cmap="coolwarm",
+            scalars="T",
+            render_points_as_spheres=False,
+            clim=[0.0, 1.0],
+            point_size=10.0,
+            opacity=0.66,
+        )
+
+        # pl.add_mesh(pvmesh, cmap="coolwarm", edge_color="Black",
+        #             show_edges=True, scalars="T",clim=[0.0,1.0],
+        #               use_transparency=False, opacity=0.5)
+
+        pl.remove_scalar_bar("T")
+        # pl.remove_scalar_bar("T1")
+
+        pl.screenshot(
+            filename="{}.png".format(filename),
+            window_size=(1250, 1250),
+            return_img=False,
+        )
+        # pl.show()
+        pl.close()
+
+
+
+
+
+
+
# Convection model / update in time
+
+expt_name = "output/Ra1e6_swarm_pnots"
+
+ad_delta_t = 0.000033  # target
+
+for step in range(0, 2): #250
+    stokes.solve(zero_init_guess=False)
+    stokes_delta_t = 5.0 * stokes.estimate_dt()
+    delta_t = stokes_delta_t
+
+    ad.solve(timestep=delta_t, zero_init_guess=True)
+
+    # update swarm / swarm variables
+
+    with swarm.access(T1):
+        T1.data[:, 0] = uw.function.evaluate(t_soln.fn, swarm.particle_coordinates.data)
+
+    # advect swarm
+    swarm.advection(v_soln.fn, delta_t)
+
+    tstats = t_soln.stats()
+    tstarstats = T1._meshVar.stats()
+
+    if uw.mpi.rank == 0:
+        print("Timestep {}, dt {}".format(step, delta_t))
+        print(tstats[2], tstats[3])
+        print(tstarstats[2], tstarstats[3])
+
+    plot_T_mesh(filename="{}_step_{}".format(expt_name, step))
+
+# savefile = "{}_ts_{}.h5".format(expt_name,step)
+# meshbox.save(savefile)
+# v_soln.save(savefile)
+# t_soln.save(savefile)
+# meshbox.generate_xdmf(savefile)
+
+
+
+
+

savefile = “output_conv/convection_cylinder.h5”.format(step) +meshbox.save(savefile) +v_soln.save(savefile) +t_soln.save(savefile) +meshbox.generate_xdmf(savefile)

+
+
+
if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshbox)
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+    tpoints = vis.meshVariable_to_pv_cloud(t_soln)
+    tpoints.point_data["T"] = vis.scalar_fn_to_pv_points(tpoints, t_soln.sym)
+    tpoint_cloud = pv.PolyData(tpoints)
+
+    spoints = vis.swarm_to_pv_cloud(swarm)
+    swarm_point_cloud = pv.PolyData(spoints)
+    with swarm.access():
+        swarm_point_cloud.point_data["T1"] = T1.data.copy()
+
+    velocity_points = vis.meshVariable_to_pv_cloud(stokes.u)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym)
+    
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.00002, opacity=0.75)
+    # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1)
+
+    # pl.add_points(point_cloud, cmap="coolwarm",
+    #               render_points_as_spheres=True,
+    #               point_size=7.5, opacity=0.25
+    #             )
+
+    pl.add_points(
+        swarm_point_cloud,
+        cmap="coolwarm",
+        render_points_as_spheres=True,
+        point_size=2.5,
+        opacity=0.5,
+        clim=[0.0, 1.0],
+    )
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        scalars="T",
+        use_transparency=False,
+        opacity=0.5,
+        clim=[0.0, 1.0],
+    )
+
+    pl.show(cpos="xy")
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Convection/Ex_Convection_Cartesian_ThermoChem.html b/main/Notebooks/Examples-Convection/Ex_Convection_Cartesian_ThermoChem.html new file mode 100644 index 0000000..8bb31a6 --- /dev/null +++ b/main/Notebooks/Examples-Convection/Ex_Convection_Cartesian_ThermoChem.html @@ -0,0 +1,950 @@ + + + + + + + + + + + Thermochemical convection — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Thermochemical convection

+ +
+
+ +
+
+
+ + + + +
+ +
+

Thermochemical convection#

+

We have a thermal convection (advection-diffusion) problem and a material-swarm mediated density variation

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+
+import underworld3 as uw
+from underworld3.systems import Stokes
+from underworld3 import function
+
+import numpy as np
+
+# options = PETSc.Options()
+# options["help"] = None
+# options["pc_type"]  = "svd"
+# options["dm_plex_check_all"] = None
+# options.getAll()
+
+
+
+
+
+
+
meshbox = uw.meshing.UnstructuredSimplexBox(
+                                            minCoords=(0.0, 0.0),
+                                            maxCoords=(1.0, 1.0),
+                                            cellSize=1.0 / 24.0,
+                                            regular=False,
+                                            )
+meshbox.dm.view()
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshbox)
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    # pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.5)
+    pl.add_mesh(pvmesh, edge_color="Black", show_edges=True)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable("U", meshbox, meshbox.dim, degree=2)
+p_soln = uw.discretisation.MeshVariable("P", meshbox, 1, degree=1)
+t_soln = uw.discretisation.MeshVariable("T", meshbox, 1, degree=3)
+t_0 = uw.discretisation.MeshVariable("T0", meshbox, 1, degree=3)
+
+
+
+
+
+
+
swarm = uw.swarm.Swarm(mesh=meshbox)
+Mat = uw.swarm.SwarmVariable("Material", swarm, 1, proxy_degree=3)
+X0 = uw.swarm.SwarmVariable("X0", swarm, meshbox.dim, _proxy=False)
+swarm.populate(fill_param=5)
+
+
+
+
+
+
+
# Create Stokes object
+
+stokes = Stokes(
+    meshbox,
+    velocityField=v_soln,
+    pressureField=p_soln,
+    solver_name="stokes",
+    verbose=False,
+)
+
+# Set solve options here (or remove default values
+# stokes.petsc_options.getAll()
+stokes.petsc_options.delValue("ksp_monitor")
+
+# Constant visc
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.viscosity = 1
+
+# Velocity boundary conditions
+stokes.add_dirichlet_bc((0.0,), "Left", (0,))
+stokes.add_dirichlet_bc((0.0,), "Right", (0,))
+stokes.add_dirichlet_bc((0.0,), "Top", (1,))
+stokes.add_dirichlet_bc((0.0,), "Bottom", (1,))
+
+
+
+
+
+
+
mMat = uw.discretisation.MeshVariable("mMat", meshbox, 1, degree=2)
+projector = uw.systems.solvers.SNES_Projection(meshbox, mMat)
+projector.smoothing = 1.0e-3
+
+
+
+
+
+
+
# Create a density structure / buoyancy force
+# gravity will vary linearly from zero at the centre
+# of the sphere to (say) 1 at the surface
+
+import sympy
+
+# Some useful coordinate stuff
+
+x = meshbox.N.x
+y = meshbox.N.y
+
+
+
+
+
+
+
# Create adv_diff object
+
+# Set some things
+k = 1.0
+h = 0.0
+r_i = 0.5
+r_o = 1.0
+
+adv_diff = uw.systems.AdvDiffusion(
+                                    meshbox,
+                                    u_Field=t_soln,
+                                    V_fn=v_soln,
+                                    solver_name="adv_diff",
+                                    order=3,
+                                    verbose=False,
+                                )
+
+adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel
+adv_diff.constitutive_model.Parameters.diffusivity = k
+adv_diff.theta = 0.5
+
+adv_diff.add_dirichlet_bc(1.0, "Bottom")
+adv_diff.add_dirichlet_bc(0.0, "Top")
+
+
+
+
+
+
+
# Define T boundary / initial conditions via a sympy function
+
+import sympy
+
+init_t = 0.01 * sympy.sin(5.0 * x) * sympy.sin(np.pi * y) + (1.0 - y)
+
+with meshbox.access(t_0, t_soln):
+    t_0.data[...] = uw.function.evaluate(init_t, t_0.coords).reshape(-1, 1)
+    t_soln.data[...] = t_0.data[...]
+
+
+
+
+
+
+
with swarm.access(Mat):
+    Mat.data[:, 0] = 0.5 + 0.5 * np.tanh(100.0 * (swarm.data[:, 1] - 0.25))
+
+
+
+
+
+
+
projector.uw_function = Mat.sym
+projector.solve()
+
+
+
+
+
+
+
expt_name = "output/Ra1e6_Rc5e5"
+
+# +ve Rc means heavy chemical component,
+# -ve Rc means light chemical component
+
+# Here we are using the projected mMat field but we
+# can switch this out for the particle field
+# to show the equivalence
+
+buoyancy_force = 1.0e6 * t_soln.fn + 5.0e5 * mMat.fn
+stokes.bodyforce = meshbox.N.j * buoyancy_force
+
+# check the stokes solve is set up and that it converges
+stokes.solve()
+
+
+
+
+
+
+
# Check the diffusion part of the solve converges
+adv_diff.solve(timestep=0.01 * stokes.estimate_dt())
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+
+if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshbox)
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+    velocity_points = vis.meshVariable_to_pv_cloud(stokes.u)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym)
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    # pl.add_mesh(pvmesh,'Black', 'wireframe')
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        scalars="T",
+        use_transparency=False,
+        opacity=0.5,
+    )
+
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=1.0e-4, opacity=0.5)
+    # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1)
+
+    # pl.add_points(pdata)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
def plot_T_mesh(filename):
+    if uw.mpi.size == 1:
+        
+        import pyvista as pv
+        import underworld3.visualisation as vis
+
+        pvmesh = vis.mesh_to_pv_mesh(meshbox)
+        pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+        pvmesh.point_data["M"] = vis.scalar_fn_to_pv_points(pvmesh, mMat.sym)
+        
+        tpoints = vis.meshVariable_to_pv_cloud(t_soln)
+        tpoints.point_data["T"] = vis.scalar_fn_to_pv_points(tpoints, t_soln.sym)
+        tpoint_cloud = pv.PolyData(tpoints)
+
+        spoints = vis.swarm_to_pv_cloud(swarm)
+        swarm_point_cloud = pv.PolyData(spoints)
+        with swarm.access():
+            swarm_point_cloud.point_data["M"] = Mat.data.copy()
+
+        velocity_points = vis.meshVariable_to_pv_cloud(stokes.u)
+        velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym)
+
+
+        pl = pv.Plotter(window_size=(750, 750))
+
+        pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.00001, opacity=0.75)
+
+        # pl.add_points(point_cloud, cmap="gray",
+        #               render_points_as_spheres=False,
+        #               point_size=10, opacity=0.5
+        #             )
+
+        pl.add_points(
+            swarm_point_cloud,
+            cmap="RdYlBu",
+            render_points_as_spheres=True,
+            point_size=7.5,
+            opacity=1.0,
+        )
+
+        pl.add_mesh(
+            pvmesh,
+            cmap="gray",
+            edge_color="Black",
+            show_edges=True,
+            scalars="M",
+            use_transparency=False,
+            opacity=0.5,
+        )
+
+        pl.remove_scalar_bar("M")
+        pl.remove_scalar_bar("mag")
+
+        pl.screenshot(
+            filename="{}.png".format(filename),
+            window_size=(1280, 1280),
+            return_img=False,
+        )
+        # pl.show()
+
+
+
+
+
+
+
# Convection model / update in time
+
+for step in range(0, 50):
+    stokes.solve(zero_init_guess=False)
+    delta_t = 3.0e-5  # 5.0*stokes.estimate_dt()
+    adv_diff.solve(timestep=delta_t, zero_init_guess=False)
+
+    # update swarm locations using v_soln
+
+    swarm.advection(v_soln.fn, delta_t, order=2, corrector=True)
+    # projector.solve(zero_init_guess=False)
+
+    # stats then loop
+    tstats = t_soln.stats()
+
+    if uw.mpi.rank == 0:
+        print("Timestep {}, dt {}".format(step, delta_t))
+    #         print(tstats)
+
+    plot_T_mesh(filename="{}_step_{}".format(expt_name, step))
+
+    # savefile = "{}_ts_{}.h5".format(expt_name,step)
+    # meshbox.save(savefile)
+    # v_soln.save(savefile)
+    # t_soln.save(savefile)
+    # meshbox.generate_xdmf(savefile)
+
+
+
+
+
+
+
if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshbox)
+    pvmesh.point_data["M"] = vis.scalar_fn_to_pv_points(pvmesh, mMat.sym)
+
+    tpoints = vis.meshVariable_to_pv_cloud(t_soln)
+    tpoints.point_data["T"] = vis.scalar_fn_to_pv_points(tpoints, t_soln.sym)
+    tpoints.point_data["M"] = vis.scalar_fn_to_pv_points(tpoints, mMat.sym)
+    tpoint_cloud = pv.PolyData(tpoints)
+
+    spoints = vis.swarm_to_pv_cloud(swarm)
+    swarm_point_cloud = pv.PolyData(spoints)
+    with swarm.access():
+        swarm_point_cloud.point_data["M"] = Mat.data.copy()
+
+    velocity_points = vis.meshVariable_to_pv_cloud(stokes.u)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym)
+
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.75e-5, opacity=0.75)
+
+
+    pl.add_points(
+        swarm_point_cloud,
+        cmap="RdYlBu",
+        render_points_as_spheres=True,
+        point_size=3.0,
+        opacity=1.0,
+    )
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="gray",
+        edge_color="Black",
+        show_edges=True,
+        scalars="M",
+        use_transparency=False,
+        opacity=0.5,
+    )
+
+    # pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.75)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Convection/Ex_Convection_Cylinder-FS.html b/main/Notebooks/Examples-Convection/Ex_Convection_Cylinder-FS.html new file mode 100644 index 0000000..8892ccc --- /dev/null +++ b/main/Notebooks/Examples-Convection/Ex_Convection_Cylinder-FS.html @@ -0,0 +1,939 @@ + + + + + + + + + + + Stokes in an annulus with adv_diff to solve T and back-in-time sampling with particles — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Stokes in an annulus with adv_diff to solve T and back-in-time sampling with particles

+ +
+
+ +
+
+
+ + + + +
+ +
+

Stokes in an annulus with adv_diff to solve T and back-in-time sampling with particles#

+

This is a simple example in which we try to instantiate two solvers on the mesh and have them use a common set of variables.

+

We set up a v, p, T system in which we will solve for a steady-state T field in response to thermal boundary conditions and then use the steady-state T field to compute a stokes flow in response.

+

The next step is to add particles at node points and sample back along the streamlines to find values of the T field at a previous time.

+

(Note, we keep all the pieces from previous increments of this problem to ensure that we don’t break something along the way)

+
+
+
# to fix trame issue
+# import nest_asyncio
+# nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+
+import underworld3 as uw
+from underworld3 import function
+
+import numpy as np
+
+
+
+
+
+
+
import os
+
+rayleigh=1.0e5
+
+output_dir = os.path.join("output","Cylinder_FS_Ra1e5_p")
+expt_name = "Cylinder_FS"
+
+os.makedirs(output_dir, exist_ok=True  )
+
+
+viz = True
+
+
+
+
+
+
+
meshball = uw.meshing.Annulus(
+    radiusInner=0.5, radiusOuter=1.0, cellSize=0.05, qdegree=3
+)
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+if viz and uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshball)
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    # pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.5)
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        use_transparency=False,
+        opacity=0.5,
+    )
+
+    pl.show()
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable("U", meshball, meshball.dim, degree=2)
+p_soln = uw.discretisation.MeshVariable("P", meshball, 1, degree=1)
+t_soln = uw.discretisation.MeshVariable("T", meshball, 1, degree=3)
+t_0 = uw.discretisation.MeshVariable("T0", meshball, 1, degree=3)
+
+
+
+
+
+
+
swarm = uw.swarm.Swarm(mesh=meshball)
+T1 = uw.swarm.SwarmVariable("Tminus1", swarm, 1)
+X1 = uw.swarm.SwarmVariable("Xminus1", swarm, 2)
+swarm.populate(fill_param=3)
+
+
+
+
+
+
+
#### Create Stokes object
+
+stokes = uw.systems.Stokes(
+    meshball, velocityField=v_soln, pressureField=p_soln, solver_name="stokes"
+)
+
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.viscosity = 1.0
+
+stokes.petsc_options.delValue("ksp_monitor")
+stokes.petsc_options.delValue("snes_monitor")
+
+# Constant visc
+stokes.viscosity = 1.0
+
+# Velocity boundary conditions
+stokes.add_essential_bc((0.0, 0.0), "Lower", (0, 1))
+
+
+
+
+
+
+
stokes.petsc_options["snes_type"] = "newtonls"
+stokes.petsc_options["ksp_type"] = "fgmres"
+
+# stokes.petsc_options.setValue("fieldsplit_velocity_pc_type", "mg")
+stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_type", "kaskade")
+stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_cycle_type", "w")
+
+stokes.petsc_options["fieldsplit_velocity_mg_coarse_pc_type"] = "svd"
+stokes.petsc_options[f"fieldsplit_velocity_ksp_type"] = "fcg"
+stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_type"] = "chebyshev"
+stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_max_it"] = 7
+stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_converged_maxits"] = None
+
+# gasm is super-fast ... but mg seems to be bulletproof
+# gamg is toughest wrt viscosity
+
+stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "gamg")
+stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "additive")
+stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v")
+
+# # mg, multiplicative - very robust ... similar to gamg, additive
+
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "mg")
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "multiplicative")
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v")
+
+
+
+
+
+
+
# Create a density structure / buoyancy force
+# gravity will vary linearly from zero at the centre
+# of the sphere to (say) 1 at the surface
+
+import sympy
+
+radius_fn = sympy.sqrt(
+    meshball.X.dot(meshball.X)
+)  # normalise by outer radius if not 1.0
+unit_rvec = meshball.X / (radius_fn)
+gravity_fn = radius_fn
+
+# Some useful coordinate stuff
+
+x = meshball.X[0]
+y = meshball.X[1]
+
+r = meshball.CoordinateSystem.R[0]
+th = meshball.CoordinateSystem.R[1]
+
+
+
+
+
+
+
# Create adv_diff object
+
+# Set some things
+k = 1.0
+h = 0.0
+r_i = 0.5
+r_o = 1.0
+
+adv_diff = uw.systems.AdvDiffusionSLCN(
+    meshball,
+    u_Field=t_soln,
+    V_fn=v_soln,
+    solver_name="adv_diff",
+    verbose=False,
+)
+
+adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel
+adv_diff.constitutive_model.Parameters.diffusivity = 1
+
+adv_diff.tolerance=1.0e-4
+
+
+
+
+
+
+
# Define T boundary conditions via a sympy function
+
+import sympy
+
+abs_r = sympy.sqrt(meshball.rvec.dot(meshball.rvec))
+init_t = 0.01 * sympy.sin(5.0 * th) * sympy.sin(np.pi * (r - r_i) / (r_o - r_i)) + (
+    r_o - r
+) / (r_o - r_i)
+
+adv_diff.add_dirichlet_bc(1.0, "Lower")
+adv_diff.add_dirichlet_bc(0.0, "Upper")
+
+with meshball.access(t_0, t_soln):
+    t_0.data[...] = uw.function.evaluate(init_t, t_0.coords).reshape(-1, 1)
+    t_soln.data[...] = t_0.data[...]
+
+
+
+
+
+
+
buoyancy_force = rayleigh * t_soln.sym[0] / (0.5) ** 3
+stokes.bodyforce = unit_rvec * buoyancy_force
+
+stokes.tolerance = 1.0e-3
+stokes.petsc_options.setValue("ksp_monitor", None)
+stokes.petsc_options.setValue("snes_monitor", None)
+
+stokes.add_essential_bc([0.0, 0.0], "Lower")  # no slip on the base
+stokes.add_natural_bc(
+    10000 * unit_rvec.dot(v_soln.sym) * unit_rvec.T, "Upper"
+)
+
+stokes.solve(verbose=False, zero_init_guess=True, picard=1 )
+
+
+
+
+
+
+
# Check the diffusion part of the solve converges
+# adv_diff.petsc_options["ksp_monitor"] = None
+adv_diff.petsc_options["snes_monitor"] = None
+
+adv_diff.solve(verbose=False, timestep=0.5 * stokes.estimate_dt())
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshball)
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) - vis.scalar_fn_to_pv_points(pvmesh, t_0.sym)
+
+    velocity_points = vis.meshVariable_to_pv_cloud(stokes.u)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym)
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    # pl.add_mesh(pvmesh,'Black', 'wireframe')
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        scalars="T",
+        use_transparency=False,
+        opacity=0.5,
+    )
+
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=1000.0/rayleigh, opacity=0.75)
+
+    # pl.add_points(pdata)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
def plot_T_mesh(filename):
+    if viz and uw.mpi.size == 1:
+        
+        import pyvista as pv
+        import underworld3.visualisation as vis
+
+        pvmesh = vis.mesh_to_pv_mesh(meshball)
+        pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+
+        tpoints = vis.meshVariable_to_pv_cloud(t_soln)
+        tpoints.point_data["T"] = vis.scalar_fn_to_pv_points(tpoints, t_soln.sym)
+        tpoint_cloud = pv.PolyData(tpoints)
+
+        velocity_points = vis.meshVariable_to_pv_cloud(stokes.u)
+        velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym)
+
+        pl = pv.Plotter(window_size=(750, 750))
+
+        pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=10.0/rayleigh, opacity=0.75)
+
+        pl.add_points(
+            tpoint_cloud,
+            cmap="coolwarm",
+            render_points_as_spheres=False,
+            point_size=10,
+            opacity=0.66,
+        )
+
+        pl.add_mesh(pvmesh, scalars="T", cmap="coolwarm", show_edges=True, opacity=0.75)
+
+        pl.remove_scalar_bar("T")
+        pl.remove_scalar_bar("mag")
+
+        pl.screenshot(
+            filename="{}.png".format(filename),
+            window_size=(1280, 1280),
+            return_img=False,
+        )
+        # pl.show()
+
+
+
+
+
+
+
ts = 0
+
+
+
+
+
+
+
# Convection model / update in time
+
+
+for step in range(0, 51):
+
+    stokes.solve(verbose=False, zero_init_guess=False, picard=0)
+
+    delta_t = adv_diff.estimate_dt(v_factor=2.0)
+    adv_diff.solve(timestep=delta_t, zero_init_guess=False)
+
+    stats = t_soln.stats()
+    stats_star = adv_diff.DuDt.psi_star[0].stats()
+    
+    if uw.mpi.rank == 0:
+        print("Timestep {}, dt {}".format(ts, delta_t))
+        print(stats)
+        print(stats_star)
+
+    if step%5 == 0:
+        plot_T_mesh(filename="{}_step_{}".format(os.path.join(output_dir, expt_name),ts))
+
+    if step%10 == 0:
+
+        meshball.write_timestep(
+                expt_name,
+                meshUpdates=True,
+                meshVars=[p_soln, v_soln, t_soln],
+                outputPath=output_dir,
+                index=ts,
+            )
+
+    ts += 1
+
+
+
+
+
+
+
+
+
if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshball)
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+
+    points = vis.meshVariable_to_pv_cloud(t_soln)
+    points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym)
+    point_cloud = pv.PolyData(points)
+
+    velocity_points = vis.meshVariable_to_pv_cloud(stokes.u)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym)
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.00005, opacity=0.75)
+    # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1)
+
+    pl.add_points(
+        point_cloud,
+        cmap="coolwarm",
+        render_points_as_spheres=True,
+        point_size=7.5,
+        opacity=0.75,
+    )
+
+    pl.add_mesh(pvmesh, scalars="T", cmap="coolwarm", opacity=1)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Convection/Ex_Convection_Cylinder.html b/main/Notebooks/Examples-Convection/Ex_Convection_Cylinder.html new file mode 100644 index 0000000..7296558 --- /dev/null +++ b/main/Notebooks/Examples-Convection/Ex_Convection_Cylinder.html @@ -0,0 +1,964 @@ + + + + + + + + + + + Stokes in a disc with adv_diff to solve T and back-in-time sampling with particles — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Stokes in a disc with adv_diff to solve T and back-in-time sampling with particles

+ +
+
+ +
+
+
+ + + + +
+ +
+

Stokes in a disc with adv_diff to solve T and back-in-time sampling with particles#

+

This is a simple example in which we try to instantiate two solvers on the mesh and have them use a common set of variables.

+

We set up a v, p, T system in which we will solve for a steady-state T field in response to thermal boundary conditions and then use the steady-state T field to compute a stokes flow in response.

+

The next step is to add particles at node points and sample back along the streamlines to find values of the T field at a previous time.

+

(Note, we keep all the pieces from previous increments of this problem to ensure that we don’t break something along the way)

+
+
+
# to fix trame issue
+# import nest_asyncio
+# nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+
+import underworld3 as uw
+from underworld3.systems import Stokes
+from underworld3 import function
+
+import numpy as np
+
+
+
+
+
+
+
meshball = uw.meshing.Annulus(
+    radiusInner=0.5, radiusOuter=1.0, cellSize=0.1, degree=1, qdegree=3
+)
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshball)
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    # pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.5)
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        use_transparency=False,
+        opacity=0.5,
+    )
+
+    pl.show()
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable("U", meshball, meshball.dim, degree=2)
+p_soln = uw.discretisation.MeshVariable("P", meshball, 1, degree=1)
+t_soln = uw.discretisation.MeshVariable("T", meshball, 1, degree=3)
+t_0 = uw.discretisation.MeshVariable("T0", meshball, 1, degree=3)
+
+
+
+
+
+
+
swarm = uw.swarm.Swarm(mesh=meshball)
+T1 = uw.swarm.SwarmVariable("Tminus1", swarm, 1)
+X1 = uw.swarm.SwarmVariable("Xminus1", swarm, 2)
+swarm.populate(fill_param=3)
+
+
+
+
+
+
+
# Create Stokes object
+
+stokes = Stokes(
+    meshball, velocityField=v_soln, pressureField=p_soln, solver_name="stokes"
+)
+
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.viscosity = 1.0
+
+
+# Set solve options here (or remove default values
+# stokes.petsc_options.getAll()
+stokes.petsc_options.delValue("ksp_monitor")
+
+# Constant visc
+stokes.viscosity = 1.0
+
+# Velocity boundary conditions
+stokes.add_dirichlet_bc((0.0, 0.0), "Upper", (0, 1))
+stokes.add_dirichlet_bc((0.0, 0.0), "Lower", (0, 1))
+
+
+
+
+
+
+
# Create a density structure / buoyancy force
+# gravity will vary linearly from zero at the centre
+# of the sphere to (say) 1 at the surface
+
+import sympy
+
+radius_fn = sympy.sqrt(
+    meshball.X.dot(meshball.X)
+)  # normalise by outer radius if not 1.0
+unit_rvec = meshball.X / (1.0e-10 + radius_fn)
+gravity_fn = radius_fn
+
+# Some useful coordinate stuff
+
+x = meshball.X[0]
+y = meshball.X[1]
+
+r = sympy.sqrt(x**2 + y**2)
+th = sympy.atan2(y + 1.0e-5, x + 1.0e-5)
+
+
+
+
+
+
+
# Create adv_diff object
+
+# Set some things
+k = 1.0
+h = 0.0
+r_i = 0.5
+r_o = 1.0
+
+adv_diff = uw.systems.AdvDiffusion(
+    meshball,
+    u_Field=t_soln,
+    V_fn=v_soln,
+    solver_name="adv_diff",
+    verbose=False,
+)
+
+adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel
+adv_diff.constitutive_model.Parameters.diffusivity = 1
+
+adv_diff.theta = 0.5
+# adv_diff.f = t_soln.fn / delta_t - t_star.fn / delta_t
+
+
+
+
+
+
+
# Define T boundary conditions via a sympy function
+
+import sympy
+
+abs_r = sympy.sqrt(meshball.rvec.dot(meshball.rvec))
+init_t = 0.01 * sympy.sin(15.0 * th) * sympy.sin(np.pi * (r - r_i) / (r_o - r_i)) + (
+    r_o - r
+) / (r_o - r_i)
+
+adv_diff.add_dirichlet_bc(1.0, "Lower")
+adv_diff.add_dirichlet_bc(0.0, "Upper")
+
+with meshball.access(t_0, t_soln):
+    t_0.data[...] = uw.function.evaluate(init_t, t_0.coords).reshape(-1, 1)
+    t_soln.data[...] = t_0.data[...]
+
+
+
+
+
+
+
buoyancy_force = 1.0e6 * t_soln.sym[0] / (0.5) ** 3
+stokes.bodyforce = unit_rvec * buoyancy_force
+
+# check the stokes solve converges
+stokes.solve()
+
+
+
+
+
+
+
# Check the diffusion part of the solve converges
+adv_diff.petsc_options["ksp_monitor"] = None
+adv_diff.petsc_options["monitor"] = None
+
+adv_diff.solve(timestep=0.00001 * stokes.estimate_dt())
+
+
+
+
+
+
+
# diff = uw.systems.Poisson(meshball, u_Field=t_soln, solver_name="diff_only")
+
+# diff.constitutive_model = uw.constitutive_models.DiffusionModel(meshball.dim)
+# diff.constitutive_model.material_properties = adv_diff.constitutive_model.Parameters(diffusivity=1)
+# diff.solve()
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+
+if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshball)
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+    # pvmesh.point_data["Ts"] = vis.scalar_fn_to_pv_points(pvmesh, adv_diff._u_star.sym)
+    # pvmesh.point_data["dT"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) - vis.scalar_fn_to_pv_points(pvmesh, adv_diff._u_star.sym)
+
+    velocity_points = vis.meshVariable_to_pv_cloud(stokes.u)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym)
+    
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    # pl.add_mesh(pvmesh,'Black', 'wireframe')
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        scalars="T",
+        use_transparency=False,
+        opacity=0.5,
+    )
+
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.0005)
+    # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1)
+
+    # pl.add_points(pdata)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
# pvmesh.point_data["Ts"].min()
+
+
+
+
+
+
+
adv_diff.petsc_options["pc_gamg_agg_nsmooths"] = 1
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshball)
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) - vis.scalar_fn_to_pv_points(pvmesh, t_0.sym)
+
+    velocity_points = vis.meshVariable_to_pv_cloud(stokes.u)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym)
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    # pl.add_mesh(pvmesh,'Black', 'wireframe')
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        scalars="T",
+        use_transparency=False,
+        opacity=0.5,
+    )
+
+    # pl.add_arrows(arrow_loc, arrow_length, mag=0.025)
+
+    # pl.add_points(pdata)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
def plot_T_mesh(filename):
+    if uw.mpi.size == 1:
+        
+        import pyvista as pv
+        import underworld3.visualisation as vis
+
+        pvmesh = vis.mesh_to_pv_mesh(meshball)
+        pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+
+        tpoints = vis.meshVariable_to_pv_cloud(t_soln)
+        tpoints.point_data["T"] = vis.scalar_fn_to_pv_points(tpoints, t_soln.sym)
+        tpoint_cloud = pv.PolyData(tpoints)
+
+        velocity_points = vis.meshVariable_to_pv_cloud(stokes.u)
+        velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym)
+
+        pl = pv.Plotter(window_size=(750, 750))
+
+        pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.00002, opacity=0.75)
+
+        pl.add_points(
+            point_cloud,
+            cmap="coolwarm",
+            render_points_as_spheres=False,
+            point_size=10,
+            opacity=0.66,
+        )
+
+        pl.add_mesh(pvmesh, "Black", "wireframe", opacity=0.75)
+
+        pl.remove_scalar_bar("T")
+        pl.remove_scalar_bar("mag")
+
+        pl.screenshot(
+            filename="{}.png".format(filename),
+            window_size=(1280, 1280),
+            return_img=False,
+        )
+        # pl.show()
+
+
+
+
+
+
+
# Convection model / update in time
+
+expt_name = "output/Cylinder_Ra1e6i"
+
+for step in range(0, 50):
+    stokes.solve()
+    delta_t = 5.0 * stokes.estimate_dt()
+    adv_diff.solve(timestep=delta_t)
+
+    # stats then loop
+    tstats = t_soln.stats()
+
+    if uw.mpi.rank == 0:
+        print("Timestep {}, dt {}".format(step, delta_t))
+    #         print(tstats)
+
+    #     plot_T_mesh(filename="{}_step_{}".format(expt_name,step))
+
+    meshball.petsc_save_checkpoint(index=step, 
+                                   meshVars=[v_soln, t_soln], 
+                                   outputPath=expt_name)
+
+
+
+
+
+
+
# savefile = "output_conv/convection_cylinder.h5".format(step)
+# meshball.save(savefile)
+# v_soln.save(savefile)
+# t_soln.save(savefile)
+# meshball.generate_xdmf(savefile)
+
+
+
+
+
+
+
if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshball)
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+
+    points = vis.meshVariable_to_pv_cloud(t_soln)
+    points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym)
+    point_cloud = pv.PolyData(points)
+
+    velocity_points = vis.meshVariable_to_pv_cloud(stokes.u)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym)
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.00005, opacity=0.75)
+    # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1)
+
+    pl.add_points(
+        point_cloud,
+        cmap="coolwarm",
+        render_points_as_spheres=True,
+        point_size=7.5,
+        opacity=0.75,
+    )
+
+    pl.add_mesh(pvmesh, scalars="T", cmap="coolwarm", opacity=1)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Convection/Ex_Convection_Disc_InternalHeat.html b/main/Notebooks/Examples-Convection/Ex_Convection_Disc_InternalHeat.html new file mode 100644 index 0000000..6b1ed01 --- /dev/null +++ b/main/Notebooks/Examples-Convection/Ex_Convection_Disc_InternalHeat.html @@ -0,0 +1,982 @@ + + + + + + + + + + + Convection in a disc with internal heating and rigid or free boundaries — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Convection in a disc with internal heating and rigid or free boundaries

+ +
+
+ +
+
+
+ + + + +
+ +
+

Convection in a disc with internal heating and rigid or free boundaries#

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+

+import underworld3 as uw
+from underworld3.systems import Stokes
+from underworld3 import function
+import os
+
+import numpy as np
+import sympy
+
+import petsc4py
+from petsc4py import PETSc
+
+
+
+
+
+
+
## Command line parameters use -uw_resolution 0.1, for example
+
+res = uw.options.getReal("resolution", default=0.1)
+Free_Slip = uw.options.getBool("free_slip", default=True)
+restart_step = uw.options.getInt("restart_step", default=0)
+max_steps = uw.options.getInt("max_steps", default=1)
+delta_eta = uw.options.getReal("delta_eta", default=1000.0)
+
+viz = True
+
+
+
+
+
+
+
uw.options.view()
+
+
+
+
+
+
+
Rayleigh = 1.0e7
+H_int = 1.0
+k = 1.0
+resI = res * 3
+r_o = 1.0
+r_i = 0.0
+
+
+# For now, assume restart is from same location !
+expt_name = f"Disc_Ra1e7_H1_deleta_{delta_eta}"
+output_dir = "output"
+
+os.makedirs(output_dir, exist_ok=True  )
+
+
+
+
+
+
+
meshball = uw.meshing.AnnulusWithSpokes(radiusOuter=r_o, radiusInner=r_i,
+                                            cellSizeOuter=res,
+                                            cellSizeInner=resI,
+                                           qdegree=3, )
+
+
+
+
+
+
+
meshball.dm.view()
+
+
+
+
+
+
+

+radius_fn = sympy.sqrt(meshball.X.dot(meshball.X)) # normalise by r_o if required
+unit_rvec = meshball.X / radius_fn
+gravity_fn = radius_fn
+
+# Some useful coordinate stuff
+
+x = meshball.N.x
+y = meshball.N.y
+
+r = sympy.sqrt(x**2 + y**2)  # cf radius_fn which is 0->1
+th = sympy.atan2(y + 1.0e-5, x + 1.0e-5)
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+if viz and uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshball)
+
+    pl = pv.Plotter(window_size=(750, 750))
+    pl.add_mesh(pvmesh, cmap="coolwarm", edge_color="Black", show_edges=True, use_transparency=False, opacity=0.5)
+
+    pl.show()
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable("U", meshball, meshball.dim, degree=2)
+p_soln = uw.discretisation.MeshVariable("P", meshball, 1, degree=1)
+t_soln = uw.discretisation.MeshVariable("T", meshball, 1, degree=3)
+t_0 = uw.discretisation.MeshVariable("T0", meshball, 1, degree=3)
+r_mesh = uw.discretisation.MeshVariable("r", meshball, 1, degree=1)
+kappa = uw.discretisation.MeshVariable("kappa", meshball, 1, degree=3, varsymbol=r"\kappa")
+
+
+
+
+
+
+
## F-K viscosity function
+
+C = sympy.log(delta_eta)
+viscosity_fn = delta_eta * sympy.exp(-C * 0)
+
+
+
+
+
+
+
# Create Stokes object
+
+stokes = Stokes(meshball, velocityField=v_soln, pressureField=p_soln, 
+                solver_name="stokes", 
+                verbose=False)
+
+# Constant viscosity
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.shear_viscosity_0 = viscosity_fn
+
+# Set solve options here (or remove default values
+stokes.tolerance = 1.0e-6
+# stokes.petsc_options.delValue("ksp_monitor")
+
+stokes.petsc_options.setValue("ksp_monitor", None)
+stokes.petsc_options.setValue("snes_monitor", None)
+
+# Velocity boundary conditions
+
+if Free_Slip:    
+    GammaN = meshball.Gamma  # boundary_normals["Upper"].value
+    # bc = sympy.Piecewise((1.0, r > 0.99 * r_o), (0.0, True))
+    stokes.add_natural_bc(
+        1.0e6 * GammaN.dot(v_soln.sym) * GammaN.T, "Upper"
+    )
+
+else:
+    stokes.add_dirichlet_bc((0.0, 0.0), "Upper")
+
+
+
+
+
+
+
meshball.Gamma
+
+
+
+
+
+
+
# Create adv_diff object
+
+adv_diff = uw.systems.AdvDiffusionSLCN(
+    meshball,
+    u_Field=t_soln,
+    V_fn=v_soln,
+    solver_name="adv_diff",
+    verbose=False,
+    order=2,
+)
+
+adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel
+
+## Flux limiting diffusivity (stabilizing term)
+
+Tgrad = meshball.vector.gradient(t_soln.sym)
+Tslope = sympy.sqrt(Tgrad.dot(Tgrad))
+Tslope_max = 25
+
+k_lim = (Tslope/Tslope_max) 
+k_eff = k * sympy.Max(1, k_lim)
+
+adv_diff.constitutive_model.Parameters.diffusivity = k
+adv_diff.f = H_int
+
+
+
+
+
+
+
## Projection to compute the diffusivity
+
+calculate_diffusivity = uw.systems.Projection(meshball, u_Field=kappa)
+calculate_diffusivity.uw_function = k_eff
+
+
+
+
+
+
+
# Define T boundary conditions via a sympy function
+
+import sympy
+
+abs_r = sympy.sqrt(meshball.rvec.dot(meshball.rvec))
+init_t = 0.25 + 0.25 * sympy.sin(7.0 * th) * sympy.sin(np.pi * (r - r_i) / (r_o - r_i)) + 0.0 * (r_o - r) / (r_o - r_i)
+
+adv_diff.add_dirichlet_bc(0.0, "Upper")
+
+with meshball.access(t_0, t_soln):
+    t_0.data[...] = uw.function.evalf(init_t, t_0.coords).reshape(-1, 1)
+    t_soln.data[...] = t_0.data[...]
+
+
+
+
+
+
+
# If restart, then pull T from there
+
+if restart_step != 0:
+    t_soln.read_timestep(expt_name, "T", restart_step, outputPath=output_dir, verbose=True)
+
+
+
+
+
+
+
with meshball.access(r_mesh):
+    r_mesh.data[:, 0] = uw.function.evalf(r, meshball.data)
+
+
+
+
+
+
+
stokes.bodyforce = unit_rvec * gravity_fn * Rayleigh * t_soln.fn
+stokes.solve(verbose=False)
+
+
+
+
+
+
+
# Check the diffusion part of the solve converges
+
+dt = 0.00001
+adv_diff.solve(timestep=dt)
+adv_diff.constitutive_model.Parameters.diffusivity = k_eff
+adv_diff.solve(timestep=dt, zero_init_guess=False)
+
+
+
+
+
+
+
calculate_diffusivity.solve()
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+if viz and uw.mpi.size == 1:
+
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshball)
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym[0])
+    pvmesh.point_data["K"] = vis.scalar_fn_to_pv_points(pvmesh, kappa.sym[0])
+
+    velocity_points = vis.meshVariable_to_pv_cloud(stokes.u)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym)
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    # pl.add_mesh(pvmesh,'Black', 'wireframe')
+
+    pl.add_mesh(
+        pvmesh, cmap="coolwarm", edge_color="Black", 
+        show_edges=True, scalars="T", 
+        use_transparency=False, opacity=1.0,
+        # clim=[0,1],
+    )
+
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.01)
+    # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1)
+
+    # pl.add_points(pdata)
+
+    pl.show(cpos="xy")
+    
+def plot_T_mesh(filename):
+
+    if viz and uw.mpi.size == 1:
+
+        import pyvista as pv
+        import underworld3.visualisation as vis
+
+        pvmesh = vis.mesh_to_pv_mesh(meshball)
+        pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+    
+        points = vis.meshVariable_to_pv_cloud(t_soln)
+        points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym)
+        point_cloud = pv.PolyData(points)
+    
+        velocity_points = vis.meshVariable_to_pv_cloud(stokes.u)
+        velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym)
+
+        pl = pv.Plotter(window_size=(750, 750))
+
+
+        pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=50 / Rayleigh)
+        pl.add_mesh(pvmesh, cmap="coolwarm", 
+                    show_edges=True,
+                    scalars="T", opacity=0.75)
+
+        pl.add_points(point_cloud, cmap="coolwarm", 
+                      render_points_as_spheres=False, 
+                      # clim=[0,1],
+                      point_size=10, opacity=0.66)
+
+
+        # pl.remove_scalar_bar("T")
+        pl.remove_scalar_bar("mag")
+
+        pl.screenshot(filename="{}.png".format(filename), window_size=(1280, 1280), return_img=False)
+        # pl.show()
+
+
+
+
+
+
+
ts = restart_step
+
+
+
+
+
+
+
# Convection model / update in time
+
+delta_t = 5.0e-5
+
+for step in range(0, max_steps ): #
+
+    stokes.solve(verbose=False, zero_init_guess=False)
+
+    calculate_diffusivity.solve()
+
+    if step%10 == 0:
+        delta_t = adv_diff.estimate_dt(v_factor=2.0, diffusivity=kappa.sym[0])
+        
+    adv_diff.solve(timestep=delta_t, zero_init_guess=False )
+
+    # stats, dt (all collective) print if rank 0, then loop
+    tstats = t_soln.stats()
+    Tgrad_stats = kappa.stats()
+    dt_estimate =  adv_diff.estimate_dt(v_factor=2.0, diffusivity=kappa.sym[0])
+
+    if uw.mpi.rank == 0:
+        print("Timestep {}, dt {} ({})".format(ts, delta_t, dt_estimate), flush=True)
+        # print(tstats)
+        # print("-----")
+        # print(Tgrad_stats)
+        # print("=====\n")
+
+        # print(tstats_star)
+
+    if ts % 10 == 0:
+        plot_T_mesh(filename="output/{}_step_{}".format(expt_name, ts))
+
+        meshball.write_timestep(
+                expt_name,
+                meshUpdates=True,
+                meshVars=[p_soln, v_soln, t_soln],
+                outputPath=output_dir,
+                index=ts,
+            )
+
+    ts += 1
+
+
+
+
+
+
+
if viz and uw.mpi.size == 1:
+
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshball)
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+    
+    points = vis.meshVariable_to_pv_cloud(t_soln)
+    points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym)
+    point_cloud = pv.PolyData(points)
+
+    velocity_points = vis.meshVariable_to_pv_cloud(stokes.u)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym)
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.01, opacity=0.75)
+    # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1)
+
+    pl.add_points(point_cloud, cmap="coolwarm", 
+                  render_points_as_spheres=True, 
+                  point_size=7.5, opacity=0.25)
+
+    pl.add_mesh(pvmesh, cmap="coolwarm", scalars="T", opacity=0.75)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Convection/Readme.html b/main/Notebooks/Examples-Convection/Readme.html new file mode 100644 index 0000000..7d3f07c --- /dev/null +++ b/main/Notebooks/Examples-Convection/Readme.html @@ -0,0 +1,582 @@ + + + + + + + + + + + Convection modelling examples — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Convection modelling examples

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

Convection modelling examples#

+

Including tests of the advection diffusion solver

+
+

Recent solver and visualization updates#

+
    +
  • [x] Ex_AdvectionDiffusionSLCN_RotationTest.py

  • +
  • [x] Ex_AdvectionDiffusionSwarm_RotationTest.py

  • +
  • [x] Ex_Convection_1_SLCN_Cartesian.py

  • +
  • [x] Ex_Convection_2_SLCN_Cartesian-TdepVisc.py

  • +
  • [x] Ex_Convection_3_SLCN_Cylindrical-TdepVisc.py

  • +
  • [x] Ex_Convection_4_SLCN_Cartesian-NL.py

  • +
  • [x] Ex_Convection_5_SLCN_Cartesian-Yield.py

  • +
  • [x] Ex_Convection_Cartesian_ThermoChem.py

  • +
  • [ ] Ex_Convection_Cartesian-Swarm.py

    +
      +
    • [ ] Marked as deprecated. Require updates around advDiff call

    • +
    +
  • +
  • [x] Ex_Convection_Cylinder.py

  • +
  • [x] Ex_Convection_Disc_InternalHeat.py

  • +
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Convection/output/README.html b/main/Notebooks/Examples-Convection/output/README.html new file mode 100644 index 0000000..630a649 --- /dev/null +++ b/main/Notebooks/Examples-Convection/output/README.html @@ -0,0 +1,519 @@ + + + + + + + + + + + Stokes model outputs — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Stokes model outputs

+ +
+
+ +
+
+
+ + + + +
+ +
+

Stokes model outputs#

+

These files are NOT under version control

+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Meshing/Manifold_S2.5-SphCoords.html b/main/Notebooks/Examples-Meshing/Manifold_S2.5-SphCoords.html new file mode 100644 index 0000000..8c984f2 --- /dev/null +++ b/main/Notebooks/Examples-Meshing/Manifold_S2.5-SphCoords.html @@ -0,0 +1,813 @@ + + + + + + + + + + + Equation systems — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Equation systems

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+
+import underworld3 as uw
+import numpy as np
+import sympy
+import meshio
+import gmsh
+
+import os
+
+os.environ["SYMPY_USE_CACHE"] = "no"
+
+
+
+
+
+
+
bubblemesh = uw.meshing.SegmentedSphere(
+    radiusOuter=1.0,
+    radiusInner=0.5,
+    cellSize=0.1,
+    numSegments=6, 
+    qdegree=3, 
+    filename="tmpWedge.msh",
+    coordinatesNative=True,
+)
+
+
+
+
+
+
+
bubblemesh.dm.view()
+
+
+
+
+
+

Equation systems#

+

We have the surface mesh, two dimensional coordinates in lat, lon, and an appropriate coordinate system definition. We can now define variables etc.

+

Note, the Cartesian coords are 3D, the lat / lon are 2D

+
+
+
V = uw.discretisation.MeshVariable("U", bubblemesh, 3, degree=2)
+P = uw.discretisation.MeshVariable("P", bubblemesh, 1, degree=1, continuous=False)
+Pc = uw.discretisation.MeshVariable("Pc",bubblemesh, 1, degree=2)
+T  = uw.discretisation.MeshVariable("T", bubblemesh, 1, degree=2)
+Tnode = uw.discretisation.MeshVariable("Tn", bubblemesh, 1, degree=1)
+Tdiff  = uw.discretisation.MeshVariable("dT", bubblemesh, 1, degree=1, continuous=False)
+Tdiffc  = uw.discretisation.MeshVariable("dTc", bubblemesh, 1, degree=1, continuous=True)
+
+
+
+
+
+
+
r, lon,lat = bubblemesh.CoordinateSystem.R
+
+
+
+
+
+
+

+t_init = sympy.sympify(sympy.sin(6 * lon) * sympy.cos(5 * lat) * sympy.cos(lat))
+
+with bubblemesh.access(T, Tnode):
+    T.data[:,0] = uw.function.evaluate(t_init, T.coords, bubblemesh.N)    
+
+
+
+
+
+
+
T.gradient()
+
+
+
+
+
+
+
slope = sympy.sqrt(T.gradient().dot(T.gradient()))
+slope
+
+
+
+
+
+
+
T.gradient()
+
+
+
+
+
+

SNES example#

+

The simplest possible solver implementation is just a projection operator

+
+
+
projector = uw.systems.Projection(bubblemesh, Tnode)
+projector.uw_function = T.sym[0]
+projector.smoothing = 1.0e-6
+
+# Test if bc's work
+projector.add_dirichlet_bc(10.0, "PoleAxisN", 0)
+projector.add_dirichlet_bc(10.0, "PolePtNo", 0)
+projector.add_dirichlet_bc(10.0, "PolePtNi", 0)
+projector.add_dirichlet_bc(-10.0, "PoleAxisS", 0)
+projector.add_dirichlet_bc(-10.0, "PolePtSo", 0)
+projector.add_dirichlet_bc(-10.0, "PolePtSi", 0)
+
+options = projector.petsc_options
+options.setValue("snes_rtol",1.0e-4)
+# options.setValue("pc_gamg_type","agg")
+# options.setValue("pc_gamg_agg_nsmooths", 1)
+# options.setValue("pc_gamg_threshold", 0.25)
+
+projector.solve()
+
+
+
+
+
+
+
projector = uw.systems.Projection(bubblemesh, Tdiff)
+projector.uw_function = slope # sympy.diff(T.sym[0], lon)        
+projector.smoothing = 1.0e-6
+
+projector.add_dirichlet_bc(0.0, "PoleAxisN", 0)
+projector.add_dirichlet_bc(0.0, "PolePtNo", 0)
+projector.add_dirichlet_bc(0.0, "PolePtNi", 0)
+projector.add_dirichlet_bc(0.0, "PoleAxisS", 0)
+projector.add_dirichlet_bc(0.0, "PolePtSo", 0)
+projector.add_dirichlet_bc(0.0, "PolePtSi", 0)
+
+options = projector.petsc_options
+options.setValue("snes_rtol",1.0e-4)
+
+projector.solve()
+
+
+
+
+
+
+
projector = uw.systems.Projection(bubblemesh, Tdiffc)
+projector.uw_function = Tdiff.sym[0] # sympy.diff(T.sym[0], lon)        
+projector.smoothing = 1.0e-6
+
+projector.add_dirichlet_bc(0.0, "PoleAxisN", 0)
+projector.add_dirichlet_bc(0.0, "PolePtNo", 0)
+projector.add_dirichlet_bc(0.0, "PolePtNi", 0)
+projector.add_dirichlet_bc(0.0, "PoleAxisS", 0)
+projector.add_dirichlet_bc(0.0, "PolePtSo", 0)
+projector.add_dirichlet_bc(0.0, "PolePtSi", 0)
+
+options = projector.petsc_options
+options.setValue("snes_rtol",1.0e-4)
+
+projector.solve()
+
+
+
+
+
+
+
if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+ 
+    # pvmesh = vis.mesh_to_pv_mesh(bubblemesh)
+    pvmesh = pv.read("tmpWedge.msh")
+    pvmesh.point_data["nT"] = vis.scalar_fn_to_pv_points(pvmesh, Tnode.sym)
+    pvmesh.point_data["dT"] = vis.scalar_fn_to_pv_points(pvmesh, Tdiff.sym)
+    pvmesh.point_data["dTc"] = vis.scalar_fn_to_pv_points(pvmesh, Tdiffc.sym)
+
+
+
+
+
+
+
pl = pv.Plotter(window_size=(750, 750))  
+clipped = pvmesh.clip(normal='x', crinkle=True)
+pl.add_mesh(
+            clipped,
+            show_edges=True,
+            scalars="dT",
+            cmap="RdYlBu",
+            opacity=1.0,  
+            # clim=[-6,6]
+        )
+pl.add_axes(labels_off=False)
+pl.show(cpos="xy")
+
+
+
+
+
+
+
## Below is some useful stuff for dmplex that we should put somewhere
+# How to get element edges etc. 
+
+
+
+
+
+
+
# pstart, pend = dmplex.getDepthStratum(0)
+# estart, eend = dmplex.getDepthStratum(1)
+# cstart, cend = dmplex.getDepthStratum(2)
+
+# dmlonlat = dmplex.getCoordinates().array.reshape(-1,2)
+
+# del_th_max = 0.0
+# for i in range(estart, eend):
+#     p1 , p2 = dmplex.getCone(i) - (pstart, pstart)
+#     del_th = np.abs(dmlonlat[p1, 0] - dmlonlat[p2,0]) / np.pi
+#     del_th_max  = max(del_th, del_th_max)
+#     if np.sign(dmlonlat[p1, 0]) != np.sign(dmlonlat[p2, 0]) :
+#         if del_th > 1:
+#             print(f"(dt-> {del_th:.3f} ",
+#                   f"{dmlonlat[p1, 0]:+4.2f} <-> {dmlonlat[p2, 0]:+4.2f} ",
+#                   f"{xyz[p1,2]}"
+#                  )
+
+# del_th   
+
+
+
+
+
+
+
# for cell in range(cstart, cend):
+#     points = set()
+#     edges = dmplex.getCone(cell)
+#     for edge in edges:
+#         points = points.union(dmplex.getCone(edge)-pstart)
+    
+#     print( f"{cell}",
+#            f"{dmlonlat[tuple(points),0]/np.pi}, {xyz[tuple(points),2]}"
+#          )
+
+
+
+
+
+
+
# Some dmplex options
+
+# https://petsc.org/release/src/dm/impls/plex/plexgmsh.c.html
+
+# options = PETSc.Options()
+# options["dm_plex_gmsh_multiple_tags"] = None
+# options["dm_plex_gmsh_spacedim"] = 2
+# options["dm_plex_gmsh_use_regions"] = None
+# options["dm_plex_gmsh_mark_vertices"] = None
+
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Meshing/Manifold_S2.5.html b/main/Notebooks/Examples-Meshing/Manifold_S2.5.html new file mode 100644 index 0000000..106a513 --- /dev/null +++ b/main/Notebooks/Examples-Meshing/Manifold_S2.5.html @@ -0,0 +1,789 @@ + + + + + + + + + + + Equation systems — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Equation systems

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+
+import underworld3 as uw
+import numpy as np
+import sympy
+import meshio
+import gmsh
+
+import os
+
+os.environ["SYMPY_USE_CACHE"] = "no"
+
+
+
+
+
+
+
bubblemesh = uw.meshing.SegmentedSphere(
+    radiusOuter=1.0,
+    radiusInner=0.98,
+    cellSize=0.1,
+    numSegments=6, 
+    qdegree=3, 
+    filename="tmpWedge.msh",
+    coordinatesNative=False,
+)
+
+
+
+
+
+
+
bubblemesh.dm.view()
+
+
+
+
+
+

Equation systems#

+

We have the surface mesh, two dimensional coordinates in lat, lon, and an appropriate coordinate system definition. We can now define variables etc.

+

Note, the Cartesian coords are 3D, the lat / lon are 2D

+
+
+
V = uw.discretisation.MeshVariable("U", bubblemesh, 3, degree=2)
+P = uw.discretisation.MeshVariable("P", bubblemesh, 1, degree=1, continuous=False)
+Pc = uw.discretisation.MeshVariable("Pc",bubblemesh, 1, degree=2)
+T  = uw.discretisation.MeshVariable("T", bubblemesh, 1, degree=3)
+Tnode = uw.discretisation.MeshVariable("Tn", bubblemesh, 1, degree=1)
+Tdiff  = uw.discretisation.MeshVariable("dT", bubblemesh, 1, degree=1, continuous=False)
+Tdiffc  = uw.discretisation.MeshVariable("dTc", bubblemesh, 1, degree=2, continuous=True)
+
+
+
+
+
+
+
r, lon,lat = bubblemesh.CoordinateSystem.R
+
+
+
+
+
+
+
t_init = sympy.sympify(sympy.sin(6 * lon) * sympy.cos(5 * lat) * sympy.cos(lat))
+
+with bubblemesh.access(T):
+    T.data[:,0] = uw.function.evaluate(t_init, T.coords, bubblemesh.N)
+
+
+
+
+
+
+
slope = sympy.sqrt(T.gradient().dot(T.gradient()))
+slope
+
+
+
+
+
+

SNES example#

+

The simplest possible solver implementation is just a projection operator

+
+
+
projector = uw.systems.Projection(bubblemesh, Tnode)
+projector.uw_function = T.sym[0]
+projector.smoothing = 1.0e-3
+
+options = projector.petsc_options
+options.setValue("snes_rtol",1.0e-4)
+# options.setValue("pc_gamg_type","agg")
+# options.setValue("pc_gamg_agg_nsmooths", 1)
+# options.setValue("pc_gamg_threshold", 0.25)
+
+projector.solve()
+
+
+
+
+
+
+
projector = uw.systems.Projection(bubblemesh, Tdiff)
+projector.uw_function = slope # sympy.diff(T.sym[0], lon)        
+projector.smoothing = 1.0e-6
+
+projector.add_dirichlet_bc(0.0, "PoleAxisN", 0)
+projector.add_dirichlet_bc(0.0, "PolePtNo", 0)
+projector.add_dirichlet_bc(0.0, "PolePtNi", 0)
+projector.add_dirichlet_bc(0.0, "PoleAxisS", 0)
+projector.add_dirichlet_bc(0.0, "PolePtSo", 0)
+projector.add_dirichlet_bc(0.0, "PolePtSi", 0)
+
+options = projector.petsc_options
+options.setValue("snes_rtol",1.0e-4)
+
+projector.solve()
+
+
+
+
+
+
+
projector = uw.systems.Projection(bubblemesh, Tdiffc)
+projector.uw_function = slope # sympy.diff(T.sym[0], lon)        
+projector.smoothing = 1.0e-6
+
+projector.add_dirichlet_bc(0.0, "PoleAxisN", 0)
+projector.add_dirichlet_bc(0.0, "PolePtNo", 0)
+projector.add_dirichlet_bc(0.0, "PolePtNi", 0)
+projector.add_dirichlet_bc(0.0, "PoleAxisS", 0)
+projector.add_dirichlet_bc(0.0, "PolePtSo", 0)
+projector.add_dirichlet_bc(0.0, "PolePtSi", 0)
+
+options = projector.petsc_options
+options.setValue("snes_rtol",1.0e-4)
+
+
+projector.solve()
+
+
+
+
+
+
+
if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+ 
+    pvmesh = vis.mesh_to_pv_mesh(bubblemesh)
+    pvmesh.point_data["nT"] = vis.scalar_fn_to_pv_points(pvmesh, Tnode.sym)
+    pvmesh.point_data["dT"] = vis.scalar_fn_to_pv_points(pvmesh, Tdiff.sym)
+    pvmesh.point_data["dTc"] = vis.scalar_fn_to_pv_points(pvmesh, Tdiffc.sym)
+
+    pl = pv.Plotter(window_size=(750, 750))
+  
+    pl.add_mesh(
+        pvmesh,
+        show_edges=True,
+        scalars="dT",
+        cmap="RdYlBu",
+        opacity=1.0,  
+        # clim=[0,1]
+    )
+
+    
+    pl.add_axes(labels_off=False)
+
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
## Below is some useful stuff for dmplex that we should put somewhere
+# How to get element edges etc. 
+
+
+
+
+
+
+
# pstart, pend = dmplex.getDepthStratum(0)
+# estart, eend = dmplex.getDepthStratum(1)
+# cstart, cend = dmplex.getDepthStratum(2)
+
+# dmlonlat = dmplex.getCoordinates().array.reshape(-1,2)
+
+# del_th_max = 0.0
+# for i in range(estart, eend):
+#     p1 , p2 = dmplex.getCone(i) - (pstart, pstart)
+#     del_th = np.abs(dmlonlat[p1, 0] - dmlonlat[p2,0]) / np.pi
+#     del_th_max  = max(del_th, del_th_max)
+#     if np.sign(dmlonlat[p1, 0]) != np.sign(dmlonlat[p2, 0]) :
+#         if del_th > 1:
+#             print(f"(dt-> {del_th:.3f} ",
+#                   f"{dmlonlat[p1, 0]:+4.2f} <-> {dmlonlat[p2, 0]:+4.2f} ",
+#                   f"{xyz[p1,2]}"
+#                  )
+
+# del_th   
+
+
+
+
+
+
+
# for cell in range(cstart, cend):
+#     points = set()
+#     edges = dmplex.getCone(cell)
+#     for edge in edges:
+#         points = points.union(dmplex.getCone(edge)-pstart)
+    
+#     print( f"{cell}",
+#            f"{dmlonlat[tuple(points),0]/np.pi}, {xyz[tuple(points),2]}"
+#          )
+
+
+
+
+
+
+
# Some dmplex options
+
+# https://petsc.org/release/src/dm/impls/plex/plexgmsh.c.html
+
+# options = PETSc.Options()
+# options["dm_plex_gmsh_multiple_tags"] = None
+# options["dm_plex_gmsh_spacedim"] = 2
+# options["dm_plex_gmsh_use_regions"] = None
+# options["dm_plex_gmsh_mark_vertices"] = None
+
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Meshing/Manifold_S2.html b/main/Notebooks/Examples-Meshing/Manifold_S2.html new file mode 100644 index 0000000..837b5d2 --- /dev/null +++ b/main/Notebooks/Examples-Meshing/Manifold_S2.html @@ -0,0 +1,825 @@ + + + + + + + + + + + Equation systems — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Equation systems

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+
+import underworld3 as uw
+import numpy as np
+import sympy
+import meshio
+import gmsh
+
+import os
+
+os.environ["SYMPY_USE_CACHE"] = "no"
+
+r_o = 1.0
+
+
+
+
+
+
+
bubblemesh = uw.meshing.SegmentedSphericalSurface2D(cellSize=0.05,numSegments=3, qdegree=3, filename="tmpManifold.msh")
+
+
+
+
+
+
+
bubblemesh.dm.view()
+
+
+
+
+
+
+
if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    # pvmesh = vis.mesh_to_pv_mesh(bubblemesh)
+    pvmesh = pv.read("tmpManifold.msh")
+    pvmesh.point_data["lon"] = bubblemesh.data[:,0]
+    pvmesh.point_data["lat"] = bubblemesh.data[:,1]
+    
+    pl = pv.Plotter()
+
+    pl.add_mesh(
+        pvmesh,
+        show_edges=True,
+        scalars="lon",
+    
+    )
+
+    pl.show(cpos="xy")
+
+
+
+
+
+

Equation systems#

+

We have the surface mesh, two dimensional coordinates in lat, lon, and an appropriate coordinate system definition. We can now define variables etc.

+

Note, the Cartesian coords are 3D, the lat / lon are 2D

+
+
+
T  = uw.discretisation.MeshVariable("T", bubblemesh, 1, degree=3)
+Tnode = uw.discretisation.MeshVariable("Tn", bubblemesh, 1, degree=1)
+Tdiff  = uw.discretisation.MeshVariable("dT", bubblemesh, 1, degree=1, continuous=False)
+Tdiffc  = uw.discretisation.MeshVariable("dTc", bubblemesh, 1, degree=1, continuous=True)
+
+
+
+
+
+
+
lon,lat = bubblemesh.CoordinateSystem.N
+
+
+
+
+
+
+
# Don't use the function.evaluate as is seems to break
+
+with bubblemesh.access(T):
+    T.data[:,0] = np.sin(T.coords[:,0]*6) * np.cos(5*T.coords[:,1]) * np.cos(T.coords[:,1])
+
+
+
+
+
+
+
slope = sympy.sqrt(T.gradient().dot(T.gradient()))
+slope
+
+
+
+
+
+

SNES example#

+

The simplest possible solver implementation is just a projection operator

+
+
+
projector = uw.systems.Projection(bubblemesh, Tnode)
+projector.uw_function = T.sym[0]
+projector.smoothing = 1.0e-3
+
+options = projector.petsc_options
+options.setValue("snes_rtol",1.0e-4)
+options.setValue("pc_gamg_type","agg")
+options.setValue("pc_gamg_agg_nsmooths", 1)
+options.setValue("pc_gamg_threshold", 0.25)
+
+projector.solve()
+
+
+
+
+
+
+
projector = uw.systems.Projection(bubblemesh, Tdiff)
+projector.uw_function = slope 
+projector.smoothing = 1.0e-6
+projector.add_dirichlet_bc((0.0,), "Poles", (0,) )
+
+options = projector.petsc_options
+options.setValue("snes_rtol",1.0e-6)
+
+projector.solve()
+
+
+
+
+
+
+
projector = uw.systems.Projection(bubblemesh, Tdiffc)
+projector.uw_function = Tdiff.sym[0]   
+projector.smoothing = 1.0e-6
+projector.add_dirichlet_bc((0.0,), "Poles", (0,) )
+
+options = projector.petsc_options
+options.setValue("snes_rtol",1.0e-6)
+
+projector.solve()
+
+
+
+
+
+
+
# if uw.mpi.size == 1:
+    
+#     import pyvista as pv
+#     import underworld3.visualisation as vis
+ 
+#     # pvmesh = pv.read("tmpManifold.msh")
+#     pvmesh = vis.mesh_to_pv_mesh("tmpManifold.msh")
+#     pvmesh.point_data["nT"] = vis.scalar_fn_to_pv_points(pvmesh, Tnode.sym)
+#     pvmesh.point_data["dT"] = vis.scalar_fn_to_pv_points(pvmesh, Tdiff.sym)
+#     pvmesh.point_data["dTc"] = vis.scalar_fn_to_pv_points(pvmesh, Tdiffc.sym)
+
+if uw.mpi.size == 1:
+    import numpy as np
+    import pyvista as pv
+    import vtk
+
+    pv.global_theme.background = "white"
+    pv.global_theme.window_size = [1050, 500]
+    pv.global_theme.antialiasing = True
+    pv.global_theme.jupyter_backend = "trame"
+    pv.global_theme.smooth_shading = True
+    pv.global_theme.camera["viewup"] = [0.0, 1.0, 0.0]
+    pv.global_theme.camera["position"] = [0.0, 0.0, 1.0]
+ 
+    pvmesh = pv.read("tmpManifold.msh")
+   
+    with bubblemesh.access():
+        pvmesh.point_data["nT"] = Tnode.data[:,0]
+        pvmesh.point_data["dT"] = uw.function.evaluate(Tdiff.sym[0], bubblemesh.data)
+        pvmesh.point_data["dTc"] = Tdiffc.data[:,0]
+
+
+
+
+
+
+
pl = pv.Plotter(window_size=(750, 750))
+
+pl.add_mesh(
+    pvmesh,
+    show_edges=True,
+    scalars="dT",
+    cmap="RdYlBu",
+    opacity=1,  
+)
+
+
+pl.add_axes(labels_off=False)
+
+
+pl.show(cpos="xy")
+
+
+
+
+
+
+
slope
+
+
+
+
+
+
+
## Below is some useful stuff for dmplex that we should put somewhere
+# How to get element edges etc. 
+
+
+
+
+
+
+
# pstart, pend = dmplex.getDepthStratum(0)
+# estart, eend = dmplex.getDepthStratum(1)
+# cstart, cend = dmplex.getDepthStratum(2)
+
+# dmlonlat = dmplex.getCoordinates().array.reshape(-1,2)
+
+# del_th_max = 0.0
+# for i in range(estart, eend):
+#     p1 , p2 = dmplex.getCone(i) - (pstart, pstart)
+#     del_th = np.abs(dmlonlat[p1, 0] - dmlonlat[p2,0]) / np.pi
+#     del_th_max  = max(del_th, del_th_max)
+#     if np.sign(dmlonlat[p1, 0]) != np.sign(dmlonlat[p2, 0]) :
+#         if del_th > 1:
+#             print(f"(dt-> {del_th:.3f} ",
+#                   f"{dmlonlat[p1, 0]:+4.2f} <-> {dmlonlat[p2, 0]:+4.2f} ",
+#                   f"{xyz[p1,2]}"
+#                  )
+
+# del_th   
+
+
+
+
+
+
+
# for cell in range(cstart, cend):
+#     points = set()
+#     edges = dmplex.getCone(cell)
+#     for edge in edges:
+#         points = points.union(dmplex.getCone(edge)-pstart)
+    
+#     print( f"{cell}",
+#            f"{dmlonlat[tuple(points),0]/np.pi}, {xyz[tuple(points),2]}"
+#          )
+
+
+
+
+
+
+
# Some dmplex options
+
+# https://petsc.org/release/src/dm/impls/plex/plexgmsh.c.html
+
+# options = PETSc.Options()
+# options["dm_plex_gmsh_multiple_tags"] = None
+# options["dm_plex_gmsh_spacedim"] = 2
+# options["dm_plex_gmsh_use_regions"] = None
+# options["dm_plex_gmsh_mark_vertices"] = None
+
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Meshing/MeshVariablePoints.html b/main/Notebooks/Examples-Meshing/MeshVariablePoints.html new file mode 100644 index 0000000..bd0410e --- /dev/null +++ b/main/Notebooks/Examples-Meshing/MeshVariablePoints.html @@ -0,0 +1,692 @@ + + + + + + + + + + + Creating a mesh (and checking the labels) — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Creating a mesh (and checking the labels)

+ +
+
+ +
+
+
+ + + + +
+ +
+

Creating a mesh (and checking the labels)#

+

This example is for the notch-localization test of Spiegelman et al. For which they supply a geometry file which gmsh can use to construct meshes at various resolutions. NOTE: we are just demonstrating the mesh here, not the solver configuration / benchmarking.

+

The .geo file is provided and we show how to make this into a .msh file and +how to read that into a uw.discretisation.Mesh object. The .geo file has header parameters to control the mesh refinement, and we provide a coarse version and the original version.

+

After that, there is some cell data which we can assign to a data structure on the elements (such as a swarm).

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+import underworld3 as uw
+import numpy as np
+import sympy
+
+
+
+
+
+
+
from underworld3.cython import petsc_discretisation
+
+
+
+
+
+
+
# %%
+mesh1 = uw.meshing.Annulus(radiusInner=0.5, radiusOuter=1.0, cellSize=0.1)
+mesh1.dm.view()
+
+
+
+
+
+
+
# %%
+# petsc_discretisation.petsc_dm_get_periodicity(mesh1.dm)
+
+
+
+
+
+
+
# %%
+petsc_discretisation.petsc_dm_set_periodicity(
+    mesh1.dm, (0.5, 3.14159, 0.0), (0.0, 0.0, 0.0), (1.0, 6.28, 0.0)
+)
+
+
+
+
+
+
+
# %%
+coodm = mesh1.dm.getCoordinateDM()
+coodm.view()
+mesh1.dm.localizeCoordinates()
+
+
+
+
+
+
+
# %%
+mesh1.dm.view()
+
+
+
+
+
+
+
# %%
+# petsc_discretisation.petsc_dm_get_periodicity(mesh1.dm)
+
+
+
+
+
+
+
# %%
+dC0 = uw.discretisation.MeshVariable(r"dC_0", mesh1, 1, degree=0, continuous=False)
+dC1 = uw.discretisation.MeshVariable(r"dC_1", mesh1, 1, degree=1, continuous=False)
+dC2 = uw.discretisation.MeshVariable(r"dC_2", mesh1, 1, degree=2, continuous=False)
+C1 = uw.discretisation.MeshVariable(r"C_1", mesh1, 1, degree=1, continuous=True)
+C2 = uw.discretisation.MeshVariable(r"C_2", mesh1, 1, degree=2, continuous=True)
+
+
+
+
+

This is how we extract cell data from the mesh. We can map it to the swarm data structure

+

check the mesh if in a notebook / serial

+
+
+
if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh1)
+
+    points = vis.meshVariable_to_pv_cloud(dC1)
+    point_cloud = pv.PolyData(points)
+
+    points = vis.meshVariable_to_pv_cloud(dC2)
+    point_cloud2 = pv.PolyData(points)
+
+    pl = pv.Plotter()
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        use_transparency=False,
+        opacity=0.5,
+    )
+    pl.add_points(
+        point_cloud,
+        color="Red",
+        render_points_as_spheres=True,
+        point_size=5,
+        opacity=0.66,
+    )
+    pl.add_points(
+        point_cloud2,
+        color="Blue",
+        render_points_as_spheres=True,
+        point_size=5,
+        opacity=0.66,
+    )
+
+    pl.show(cpos="xy")
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Meshing/output/README.html b/main/Notebooks/Examples-Meshing/output/README.html new file mode 100644 index 0000000..7b14806 --- /dev/null +++ b/main/Notebooks/Examples-Meshing/output/README.html @@ -0,0 +1,519 @@ + + + + + + + + + + + Meshing model outputs — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Meshing model outputs

+ +
+
+ +
+
+
+ + + + +
+ +
+

Meshing model outputs#

+

These files are NOT under version control

+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-NavierStokes/Ex_NavierStokesRotationTest.html b/main/Notebooks/Examples-NavierStokes/Ex_NavierStokesRotationTest.html new file mode 100644 index 0000000..3fff19d --- /dev/null +++ b/main/Notebooks/Examples-NavierStokes/Ex_NavierStokesRotationTest.html @@ -0,0 +1,978 @@ + + + + + + + + + + + Navier Stokes test: boundary driven ring with step change in boundary conditions — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Navier Stokes test: boundary driven ring with step change in boundary conditions

+ +
+
+ +
+
+
+ + + + +
+ + + + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Benchmarks_NS_DFG_2d.html b/main/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Benchmarks_NS_DFG_2d.html new file mode 100644 index 0000000..2dc582d --- /dev/null +++ b/main/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Benchmarks_NS_DFG_2d.html @@ -0,0 +1,1239 @@ + + + + + + + + + + + Navier Stokes test: flow around a circular inclusion (2D) — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Navier Stokes test: flow around a circular inclusion (2D)

+ +
+
+ +
+
+
+ + + + +
+ + + + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Benchmarks_NS_DFG_2d_SLCN.html b/main/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Benchmarks_NS_DFG_2d_SLCN.html new file mode 100644 index 0000000..ed18390 --- /dev/null +++ b/main/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Benchmarks_NS_DFG_2d_SLCN.html @@ -0,0 +1,1157 @@ + + + + + + + + + + + Navier Stokes test: flow around a circular inclusion (2D) — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Navier Stokes test: flow around a circular inclusion (2D)

+ +
+
+ +
+
+
+ + + + +
+ + + + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Disk_Coriolis.html b/main/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Disk_Coriolis.html new file mode 100644 index 0000000..d651e84 --- /dev/null +++ b/main/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Disk_Coriolis.html @@ -0,0 +1,951 @@ + + + + + + + + + + + Cylindrical Stokes with Coriolis term (out of plane) — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Cylindrical Stokes with Coriolis term (out of plane)

+ +
+ +
+
+ + + + +
+ +
+

Cylindrical Stokes with Coriolis term (out of plane)#

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+
+import underworld3 as uw
+import numpy as np
+import sympy
+
+
+
+
+
+
+
model = 1
+
+if model == 1:
+    free_slip = False
+    expt_name = "NS_flow_coriolis_disk_no_slip"
+
+elif model == 2:
+    free_slip = True
+    expt_name = "NS_flow_coriolis_disk_free_slip"
+
+
+
+
+
+
+
meshball = uw.meshing.Annulus(
+    radiusOuter=1.0,
+    radiusInner=0.0,
+    cellSize=0.05,
+    qdegree=3,
+    filename="tmp_CoriolisDisk.msh",
+)
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable("U", meshball, 2, degree=2)
+p_soln = uw.discretisation.MeshVariable("P", meshball, 1, degree=1)
+t_soln = uw.discretisation.MeshVariable("T", meshball, 1, degree=3)
+
+v_soln_1 = uw.discretisation.MeshVariable("U_1", meshball, meshball.dim, degree=2)
+vorticity = uw.discretisation.MeshVariable("omega", meshball, 1, degree=1)
+
+
+
+
+
+
+
swarm = uw.swarm.Swarm(mesh=meshball)
+v_star = uw.swarm.SwarmVariable("Vs", swarm, meshball.dim, proxy_degree=3)
+remeshed = uw.swarm.SwarmVariable("Vw", swarm, 1, proxy_degree=3, dtype="int")
+X_0 = uw.swarm.SwarmVariable("X0", swarm, meshball.dim, _proxy=False)
+
+swarm.populate(fill_param=4)
+
+
+
+
+
+
+
# Create a density structure / buoyancy force
+# gravity will vary linearly from zero at the centre
+# of the sphere to (say) 1 at the surface
+
+import sympy
+
+radius_fn = sympy.sqrt(
+    meshball.rvec.dot(meshball.rvec)
+)  # normalise by outer radius if not 1.0
+
+unit_rvec = meshball.CoordinateSystem.unit_e_0
+gravity_fn = radius_fn
+
+# Some useful coordinate stuff
+
+x = meshball.N.x
+y = meshball.N.y
+
+r = sympy.sqrt(x**2 + y**2)
+th = sympy.atan2(y + 1.0e-5, x + 1.0e-5)
+
+#
+
+Rayleigh = 1.0e2
+
+
+
+
+
+
+
# Surface-drive flow, use this bc
+
+# vtheta = r * sympy.sin(th)
+# vx = -vtheta*sympy.sin(th)
+# vy =  vtheta*sympy.cos(th)
+
+
+
+
+
+
+
# Create NS object
+
+navier_stokes = uw.systems.NavierStokesSwarm(
+    meshball,
+    velocityField=v_soln,
+    pressureField=p_soln,
+    velocityStar_fn=v_star.sym,
+    rho=1.0,
+    theta=0.5,
+    verbose=False,
+    projection=False,
+    solver_name="navier_stokes",
+)
+
+navier_stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel(meshball.dim)
+navier_stokes.constitutive_model.Parameters.viscosity = 1
+navier_stokes.rho = 1000.0
+navier_stokes.theta = 0.5
+navier_stokes.bodyforce = sympy.Matrix([0, 0])
+
+navier_stokes.saddle_preconditioner = (
+    1.0 / navier_stokes.constitutive_model.Parameters.viscosity
+)
+
+hw = 1000.0 / meshball.get_min_radius()
+surface_fn = sympy.exp(-((r - 1.0) ** 2) * hw)
+free_slip_penalty = (
+    1.0e4 * Rayleigh * v_soln.sym.dot(unit_rvec) * unit_rvec * surface_fn
+)
+
+# Velocity boundary conditions
+
+if free_slip:
+    free_slip_penalty = (
+        1.0e4 * Rayleigh * v_soln.sym.dot(unit_rvec) * unit_rvec * surface_fn
+    )
+else:
+    free_slip_penalty = 0.0
+    navier_stokes.add_dirichlet_bc((0.0, 0.0), "Upper", (0, 1))
+
+navier_stokes.add_dirichlet_bc((0.0, 0.0), "Centre", (0, 1))
+
+v_theta = (
+    navier_stokes.theta * navier_stokes.u.sym
+    + (1.0 - navier_stokes.theta) * navier_stokes.u_star_fn
+)
+
+
+
+
+
+
+
t_init = sympy.cos(3 * th)
+
+
+
+
+
+
+
# Write density into a variable for saving
+
+with meshball.access(t_soln):
+    t_soln.data[:, 0] = uw.function.evaluate(t_init, t_soln.coords, meshball.N)
+
+
+
+
+
+
+
navier_stokes.bodyforce = Rayleigh * unit_rvec * t_init  # minus * minus
+navier_stokes.bodyforce -= free_slip_penalty
+
+
+
+
+
+
+
navier_stokes.solve(timestep=10.0)
+
+with meshball.access():
+    v_inertial = v_soln.data.copy()
+
+with swarm.access(v_star, remeshed, X_0):
+    v_star.data[...] = uw.function.evaluate(v_soln.fn, swarm.data)
+    X_0.data[...] = swarm.data[...]
+
+
+
+
+
+
+
swarm.advection(v_soln.fn, delta_t=navier_stokes.estimate_dt(), corrector=False)
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+
+def plot_V_mesh(filename):
+    if uw.mpi.size == 1:
+
+        import pyvista as pv
+        import underworld3.visualisation as vis
+
+        pvmesh = vis.mesh_to_pv_mesh(meshball)
+        pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+
+        velocity_points = vis.meshVariable_to_pv_cloud(navier_stokes.u)
+        velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, navier_stokes.u.sym)
+        
+        pl = pv.Plotter(window_size=(1000, 750))
+        pl.camera.SetPosition(0.0001, 0.0001, 4.0)
+
+        # pl.add_mesh(pvmesh,'Black', 'wireframe')
+        pl.add_mesh(
+            pvmesh,
+            cmap="coolwarm",
+            edge_color="Black",
+            show_edges=True,
+            use_transparency=False,
+            opacity=0.5,
+        )
+        pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.2)
+
+        pl.screenshot(
+            filename="{}.png".format(filename),
+            window_size=(2560, 2560),
+            return_img=False,
+        )
+
+
+
+
+
+
+
ts = 0
+swarm_loop = 10
+
+Omega = 2.0 * meshball.N.k
+navier_stokes.bodyforce = Rayleigh * unit_rvec * t_init  # minus * minus
+navier_stokes.bodyforce -= free_slip_penalty
+navier_stokes.bodyforce -= (
+    2.0 * navier_stokes.rho * meshball.vector.cross(Omega, v_theta)
+)
+
+
+
+
+
+
+
for step in range(0, 250):
+    delta_t = 5.0 * navier_stokes.estimate_dt()
+
+    navier_stokes.solve(timestep=delta_t, zero_init_guess=False)
+
+    dv_fn = v_soln.fn - v_soln_1.fn
+    _, _, _, _, _, _, deltaV = meshball.stats(dv_fn.dot(dv_fn))
+
+    with meshball.access(v_soln_1):
+        v_soln_1.data[...] = 0.5 * v_soln_1.data[...] + 0.5 * v_soln.data[...]
+
+    with swarm.access(v_star):
+        v_star.data[...] = uw.function.evaluate(v_soln.fn, swarm.data)
+
+    swarm.advection(v_soln.fn, delta_t=delta_t, corrector=False)
+
+    # Restore a subset of points to start
+    offset_idx = step % swarm_loop
+
+    with swarm.access(swarm.particle_coordinates, remeshed):
+        remeshed.data[...] = 0
+        remeshed.data[offset_idx::swarm_loop, :] = 1
+        swarm.data[offset_idx::swarm_loop, :] = X_0.data[offset_idx::swarm_loop, :]
+
+    # re-calculate v history for remeshed particles
+    # Note, they may have moved procs after the access manager closed
+    # so we re-index
+
+    with swarm.access(v_star, remeshed):
+        idx = np.where(remeshed.data == 1)[0]
+        v_star.data[idx] = uw.function.evaluate(v_soln.fn, swarm.data[idx])
+
+    if uw.mpi.rank == 0:
+        print("Timestep {}, dt {}, deltaV {}".format(ts, delta_t, deltaV))
+
+    if ts % 1 == 0:
+        # nodal_vorticity_from_v.solve()
+        plot_V_mesh(filename="output/{}_step_{}".format(expt_name, ts))
+
+        # savefile = "output/{}_ts_{}.h5".format(expt_name,step)
+        # meshball.save(savefile)
+        # v_soln.save(savefile)
+        # p_soln.save(savefile)
+        # vorticity.save(savefile)
+        # meshball.generate_xdmf(savefile)
+
+    ts += 1
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshball)
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+
+    velocity_points = vis.meshVariable_to_pv_cloud(navier_stokes.u)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, navier_stokes.u.sym)
+    
+    pl = pv.Plotter(window_size=(1000, 750))
+
+    # pl.add_mesh(pvmesh,'Black', 'wireframe')
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        use_transparency=False,
+        opacity=0.5,
+    )
+    pl.add_arrows(arrow_loc, arrow_length, mag=0.2)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
pl.camera.GetClippingRange()
+
+
+
+
+
+
+
((v_inertial - usol) ** 2).mean()
+
+
+
+
+
+
+
v_inertial.max()
+
+
+
+
+
+
+

#

+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Disk_FreeSlipBCs-Coriolis.html b/main/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Disk_FreeSlipBCs-Coriolis.html new file mode 100644 index 0000000..0a6703a --- /dev/null +++ b/main/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Disk_FreeSlipBCs-Coriolis.html @@ -0,0 +1,1035 @@ + + + + + + + + + + + Cylindrical Stokes with Coriolis term (out of plane) — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Cylindrical Stokes with Coriolis term (out of plane)

+ +
+
+ +
+
+
+ + + + +
+ +
+

Cylindrical Stokes with Coriolis term (out of plane)#

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+
+import underworld3 as uw
+import numpy as np
+import sympy
+
+
+
+
+
+
+
expt_name = "NS_FS_flow_coriolis_disk_500_iii"
+
+
+
+
+
+
+
import meshio
+
+# meshball = uw.meshes.SphericalShell(
+#     dim=2, radius_outer=1.0, radius_inner=0.0, cell_size=0.075, degree=1, verbose=False
+# )
+
+meshball = uw.meshing.Annulus(radiusOuter=1.0, radiusInner=0.0, cellSize=0.05, degree=1, centre=False, verbosity=True)
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable("U", meshball, 2, degree=2)
+p_soln = uw.discretisation.MeshVariable("P", meshball, 1, degree=1)
+t_soln = uw.discretisation.MeshVariable("T", meshball, 1, degree=3)
+r = uw.discretisation.MeshVariable("R", meshball, 1, degree=1)
+
+
+v_soln_1 = uw.discretisation.MeshVariable("U_1", meshball, meshball.dim, degree=2)
+vorticity = uw.discretisation.MeshVariable("omega", meshball, 1, degree=1)
+
+
+
+
+
+
+
swarm = uw.swarm.Swarm(mesh=meshball)
+v_star = uw.swarm.SwarmVariable("Vs", swarm, meshball.dim, proxy_degree=3)
+remeshed = uw.swarm.SwarmVariable("Vw", swarm, 1, dtype="int", _proxy=False)
+X_0 = uw.swarm.SwarmVariable("X0", swarm, meshball.dim, _proxy=False)
+
+swarm.populate(fill_param=4)
+
+
+
+
+
+
+
# Create a density structure / buoyancy force
+# gravity will vary linearly from zero at the centre
+# of the sphere to (say) 1 at the surface
+
+import sympy
+
+radius_fn = sympy.sqrt(
+    meshball.rvec.dot(meshball.rvec)
+)  # normalise by outer radius if not 1.0
+unit_rvec = meshball.rvec / (1.0e-10 + radius_fn)
+gravity_fn = radius_fn
+
+# Some useful coordinate stuff
+
+x = meshball.N.x
+y = meshball.N.y
+
+# r  = sympy.sqrt(x**2+y**2)
+th = sympy.atan2(y + 1.0e-5, x + 1.0e-5)
+
+#
+Rayleigh = 1.0e2
+
+#
+hw = 1000.0 / 0.075
+surface_fn = sympy.exp(-(((r.fn - 1.0) / 1.0) ** 2) * hw)
+
+
+
+
+
+
+
orientation_wrt_z = sympy.atan2(y + 1.0e-10, x + 1.0e-10)
+v_rbm_z_x = -r.fn * sympy.sin(orientation_wrt_z) * meshball.N.i
+v_rbm_z_y = r.fn * sympy.cos(orientation_wrt_z) * meshball.N.j
+v_rbm_z = v_rbm_z_x + v_rbm_z_y
+
+
+
+
+
+
+
# Create NS object
+
+navier_stokes = uw.systems.NavierStokesSwarm(
+    meshball,
+    velocityField=v_soln,
+    pressureField=p_soln,
+    velocityStar_fn=v_star.fn,
+    u_degree=v_soln.degree,
+    p_degree=p_soln.degree,
+    rho=1.0,
+    theta=0.5,
+    verbose=False,
+    projection=True,
+    solver_name="navier_stokes",
+)
+
+navier_stokes.petsc_options.delValue(
+    "ksp_monitor"
+)  # We can flip the default behaviour at some point
+navier_stokes._u_star_projector.petsc_options.delValue("ksp_monitor")
+navier_stokes._u_star_projector.petsc_options["snes_rtol"] = 1.0e-2
+navier_stokes._u_star_projector.petsc_options["snes_type"] = "newtontr"
+navier_stokes._u_star_projector.smoothing = 0.0  # navier_stokes.viscosity * 1.0e-6
+navier_stokes._u_star_projector.penalty = 0.0
+
+# Constant visc
+
+navier_stokes.rho = 1.0
+navier_stokes.theta = 0.5
+navier_stokes.penalty = 0.0
+navier_stokes.viscosity = 1.0
+
+# Free slip condition by penalizing radial velocity at the surface (non-linear term)
+free_slip_penalty = 1.0e4 * Rayleigh * v_soln.fn.dot(unit_rvec) * unit_rvec * surface_fn
+
+# Velocity boundary conditions
+
+# navier_stokes.add_dirichlet_bc( (0.0, 0.0), "Upper",  (0,1))
+# navier_stokes.add_dirichlet_bc( (0.0, 0.0), "Centre", (0,1))
+
+v_theta = (
+    navier_stokes.theta * navier_stokes.u.fn
+    + (1.0 - navier_stokes.theta) * navier_stokes.u_star_fn
+)
+
+
+
+
+
+
+
nodal_vorticity_from_v = uw.systems.Projection(meshball, vorticity)
+nodal_vorticity_from_v.uw_function = sympy.vector.curl(v_soln.fn).dot(meshball.N.k)
+nodal_vorticity_from_v.smoothing = 1.0e-3
+
+
+
+
+
+
+
t_init = sympy.cos(3 * th)
+
+
+
+
+
+
+
with meshball.access(r):
+    r.data[:, 0] = uw.function.evaluate(
+        sympy.sqrt(x**2 + y**2), meshball.data
+    )  # cf radius_fn which is 0->1
+
+# Write density into a variable for saving
+
+with meshball.access(t_soln):
+    t_soln.data[:, 0] = uw.function.evaluate(t_init, t_soln.coords)
+
+
+
+
+
+
+
navier_stokes.bodyforce = Rayleigh * unit_rvec * t_init  # minus * minus
+navier_stokes.bodyforce -= free_slip_penalty  # + solid_body_penalty
+
+v_proj = navier_stokes._u_star_projector.u
+free_slip_penalty_p = 100 * v_proj.fn.dot(unit_rvec) * unit_rvec * surface_fn
+navier_stokes._u_star_projector.F0 = free_slip_penalty_p  # + solid_body_penalty_p)
+
+
+
+
+
+
+
navier_stokes.solve(timestep=10.0)
+nodal_vorticity_from_v.solve()
+
+with meshball.access():
+    v_inertial = v_soln.data.copy()
+
+with swarm.access(v_star, remeshed, X_0):
+    v_star.data[...] = uw.function.evaluate(v_soln.fn, swarm.data)
+    X_0.data[...] = swarm.data[...]
+
+
+
+
+
+
+
swarm.advection(v_soln.fn, delta_t=navier_stokes.estimate_dt(), corrector=False)
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+
+
+def plot_V_mesh(filename):
+
+    if uw.mpi.size == 1:
+
+        import pyvista as pv
+        import underworld3.visualisation as vis
+
+        pvmesh = vis.mesh_to_pv_mesh(meshball)
+        pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+        pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym)
+        pvmesh.point_data["Om"] = vis.scalar_fn_to_pv_points(pvmesh, vorticity.sym)
+
+        velocity_points = vis.meshVariable_to_pv_cloud(navier_stokes.u)
+        velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, navier_stokes.u.sym)
+
+        pl = pv.Plotter(window_size=(1000, 750))
+        pl.camera.SetPosition(0.0001, 0.0001, 4.0)
+
+        # pl.add_mesh(pvmesh,'Black', 'wireframe')
+        pl.add_mesh(
+            pvmesh,
+            cmap="coolwarm",
+            edge_color="Black",
+            show_edges=True,
+            scalars="Om",
+            use_transparency=False,
+            opacity=0.5,
+        )
+        pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.03)
+
+        pl.screenshot(
+            filename="{}.png".format(filename),
+            window_size=(2560, 2560),
+            return_img=False,
+        )
+
+        pl.close()
+
+        del pl
+
+
+
+
+
+
+
ts = 0
+swarm_loop = 5
+
+
+
+
+
+
+
vorticity.fn
+
+
+
+
+
+
+
for step in range(0, 10):
+
+    Omega_0 = 50.0 * min(ts / 10, 1.0)
+    Coriolis = (
+        2.0 * Omega_0 * navier_stokes.rho * sympy.vector.cross(meshball.N.k, v_theta)
+    )
+
+    navier_stokes.bodyforce = Rayleigh * unit_rvec * t_init  # minus * minus
+    navier_stokes.bodyforce -= free_slip_penalty
+    navier_stokes.bodyforce -= Coriolis * (1.0 - surface_fn)
+
+    delta_t = 1.0 * navier_stokes.estimate_dt()
+
+    navier_stokes.solve(timestep=delta_t, zero_init_guess=False)
+    nodal_vorticity_from_v.solve()
+
+    _, z_ns, _, _, _, _, _ = meshball.stats(v_soln.fn.dot(v_rbm_z))
+    print("Rigid body: {}".format(z_ns))
+
+    dv_fn = v_soln.fn - v_soln_1.fn
+    _, _, _, _, _, _, deltaV = meshball.stats(dv_fn.dot(dv_fn))
+
+    with meshball.access(v_soln_1):
+        v_soln_1.data[...] = v_soln.data[...]
+
+    with swarm.access(v_star):
+        v_star.data[...] = (
+            0.5 * uw.function.evaluate(v_soln.fn, swarm.data) + 0.5 * v_star.data[...]
+        )
+
+    swarm.advection(v_soln.fn, delta_t=delta_t, corrector=False)
+
+    # Restore a subset of points to start
+    offset_idx = step % swarm_loop
+
+    with swarm.access(swarm.particle_coordinates, remeshed):
+        remeshed.data[...] = 0
+        remeshed.data[offset_idx::swarm_loop, :] = 1
+        swarm.data[offset_idx::swarm_loop, :] = X_0.data[offset_idx::swarm_loop, :]
+
+    # re-calculate v history for remeshed particles
+    # Note, they may have moved procs after the access manager closed
+    # so we re-index
+
+    with swarm.access(v_star, remeshed):
+        idx = np.where(remeshed.data == 1)[0]
+        v_star.data[idx] = uw.function.evaluate(v_soln.fn, swarm.data[idx])
+
+    if uw.mpi.rank == 0:
+        print("Timestep {}, dt {}, deltaV {}".format(ts, delta_t, deltaV))
+
+    if ts % 1 == 0:
+        # nodal_vorticity_from_v.solve()
+        plot_V_mesh(filename="output/{}_step_{}".format(expt_name, ts))
+
+        # savefile = "output/{}_ts_{}.h5".format(expt_name,step)
+        # meshball.save(savefile)
+        # v_soln.save(savefile)
+        # p_soln.save(savefile)
+        # vorticity.save(savefile)
+        # meshball.generate_xdmf(savefile)
+
+    navier_stokes._u_star_projector.smoothing = navier_stokes.viscosity * 1.0e-6
+
+    ts += 1
+
+
+
+
+
+
+
navier_stokes._p_f0
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+
+
+if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshball)
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+    pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym)
+    pvmesh.point_data["Om"] = vis.scalar_fn_to_pv_points(pvmesh, vorticity.sym)
+
+    velocity_points = vis.meshVariable_to_pv_cloud(navier_stokes.u)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, navier_stokes.u.sym)
+
+    pl = pv.Plotter(window_size=[1000, 1000])
+
+    # pl.add_mesh(pvmesh,'Black', 'wireframe')
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        scalars="Om",
+        use_transparency=False,
+        opacity=0.5,
+    )
+    pl.add_arrows(arrow_loc, arrow_length, mag=0.05)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
meshball.stats(
+    sympy.vector.cross(Omega, v_soln.fn).dot(sympy.vector.cross(Omega, v_soln.fn))
+)
+
+
+
+
+
+
+
meshball.stats(v_soln.fn.dot(v_rbm_z))
+
+
+
+
+
+
+
meshball.stats(v_soln.fn.dot(v_soln.fn))
+
+
+
+
+
+
+
meshball.stats(v_soln.fn.dot(v_rbm_z))
+
+
+
+
+
+
+
meshball.stats((v_soln.fn + 0.015 * v_rbm_z).dot(v_rbm_z))
+
+
+
+
+
+
+
meshball.stats(sympy.vector.cross(Omega, v_soln.fn).dot(v_rbm_z))
+
+
+
+
+
+
+
sympy.vector.cross(Omega, v_soln.fn)
+
+
+
+
+
+
+
_, z_ns, _, _, _, _, _ = meshball.stats(v_soln.fn.dot(v_rbm_z))
+print("Rigid body: {}".format(z_ns))
+
+
+
+
+
+
+
x_ns_
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Disk_NoSlipBCs-Coriolis_ss.html b/main/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Disk_NoSlipBCs-Coriolis_ss.html new file mode 100644 index 0000000..c0d8b56 --- /dev/null +++ b/main/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Disk_NoSlipBCs-Coriolis_ss.html @@ -0,0 +1,986 @@ + + + + + + + + + + + Cylindrical Stokes with Coriolis term (out of plane) — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Cylindrical Stokes with Coriolis term (out of plane)

+ +
+ +
+
+ + + + +
+ +
+

Cylindrical Stokes with Coriolis term (out of plane)#

+

We implement Stokes/Navier-Stokes flow in a disc with rigid bc’s and take into account the effect of Coriolis terms +\(2\Omega \times \mathbf{u}\).

+

The non-linear implementation in Stokes flow relies on the Newton (SNES) solver but the structure of the solution (with strong Coriolis terms) +is very different from the Stokes flow pattern (especially the pressure gradients) and so we instead set up a Navier-Stokes problem in which +we can down-weight the inertial effects but retain a time-evolution terms to approach the solution. In this example, I am using the same +trick as for the SS benchmark case where I try to suppress the time dependent term in the Navier-Stokes equation and replace timesteps with +pseudo timesteps.

+

This works best if we spin-up the rotation gradually.

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+
+import underworld3 as uw
+import numpy as np
+import sympy
+
+
+
+
+
+
+
expt_name = "SS_NS_flow_coriolis_10"
+
+
+
+
+
+
+
import meshio
+
+# meshball = uw.meshes.SphericalShell(
+#     dim=2, radius_outer=1.0, radius_inner=0.0, cell_size=0.05, degree=1, verbose=True
+# )
+
+meshball = uw.meshing.Annulus(radiusOuter=1.0, radiusInner=0.0, cellSize=0.05, degree=1, centre=False, verbosity=True)
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable("U", meshball, 2, degree=2)
+p_soln = uw.discretisation.MeshVariable("P", meshball, 1, degree=1)
+t_soln = uw.discretisation.MeshVariable("T", meshball, 1, degree=3)
+
+v_soln_1 = uw.discretisation.MeshVariable("U_1", meshball, meshball.dim, degree=2)
+vorticity = uw.discretisation.MeshVariable("omega", meshball, 1, degree=1)
+
+
+
+
+
+
+
swarm = uw.swarm.Swarm(mesh=meshball)
+v_star = uw.swarm.SwarmVariable("Vs", swarm, meshball.dim, proxy_degree=3)
+remeshed = uw.swarm.SwarmVariable("Vw", swarm, 1, proxy_degree=3, dtype="int")
+X_0 = uw.swarm.SwarmVariable("X0", swarm, meshball.dim, _proxy=False)
+
+swarm.populate(fill_param=4)
+
+
+
+
+
+
+
# Create a density structure / buoyancy force
+# gravity will vary linearly from zero at the centre
+# of the sphere to (say) 1 at the surface
+
+import sympy
+
+radius_fn = sympy.sqrt(
+    meshball.rvec.dot(meshball.rvec)
+)  # normalise by outer radius if not 1.0
+unit_rvec = meshball.rvec / (1.0e-10 + radius_fn)
+gravity_fn = radius_fn
+
+# Some useful coordinate stuff
+
+x = meshball.N.x
+y = meshball.N.y
+
+r = sympy.sqrt(x**2 + y**2)
+th = sympy.atan2(y + 1.0e-5, x + 1.0e-5)
+
+#
+
+Rayleigh = 1.0e2
+
+
+
+
+
+
+
# Surface-drive flow, use this bc
+
+# vtheta = r * sympy.sin(th)
+# vx = -vtheta*sympy.sin(th)
+# vy =  vtheta*sympy.cos(th)
+
+
+
+
+
+
+
# Create NS object
+
+navier_stokes = uw.systems.NavierStokesSwarm(
+    meshball,
+    velocityField=v_soln,
+    pressureField=p_soln,
+    velocityStar_fn=v_star.fn,
+    u_degree=v_soln.degree,
+    p_degree=p_soln.degree,
+    rho=1.0,
+    theta=0.5,
+    verbose=False,
+    projection=True,
+    solver_name="navier_stokes",
+)
+
+navier_stokes.petsc_options.delValue(
+    "ksp_monitor"
+)  # We can flip the default behaviour at some point
+navier_stokes._u_star_projector.petsc_options.delValue("ksp_monitor")
+navier_stokes._u_star_projector.petsc_options["snes_rtol"] = 1.0e-2
+navier_stokes._u_star_projector.petsc_options["snes_type"] = "newtontr"
+navier_stokes._u_star_projector.smoothing = 0.0  # navier_stokes.viscosity * 1.0e-6
+navier_stokes._u_star_projector.penalty = 0.0001
+
+# Here we replace the time dependence with the steady state advective transport term
+# to lean towards steady state solutions
+
+navier_stokes.UF0 = (
+    -navier_stokes.rho * (v_soln.fn - v_soln_1.fn) / navier_stokes.delta_t
+)
+
+# Constant visc
+
+navier_stokes.rho = 1000.0
+navier_stokes.theta = 0.5
+navier_stokes.penalty = 0.0
+navier_stokes.viscosity = 1.0
+navier_stokes.bodyforce = 1.0e-32 * meshball.N.i
+navier_stokes._Ppre_fn = 1.0 / (
+    navier_stokes.viscosity + navier_stokes.rho / navier_stokes.delta_t
+)
+
+# Velocity boundary conditions
+
+navier_stokes.add_dirichlet_bc((0.0, 0.0), "Upper", (0, 1))
+navier_stokes.add_dirichlet_bc((0.0, 0.0), "Centre", (0, 1))
+
+v_theta = (
+    navier_stokes.theta * navier_stokes.u.fn
+    + (1.0 - navier_stokes.theta) * navier_stokes.u_star_fn
+)
+
+
+
+
+
+
+
t_init = sympy.cos(3 * th)
+
+
+
+
+
+
+
# Write density into a variable for saving
+
+with meshball.access(t_soln):
+    t_soln.data[:, 0] = uw.function.evaluate(t_init, t_soln.coords)
+    print(t_soln.data.min(), t_soln.data.max())
+
+
+
+
+
+
+
navier_stokes.bodyforce = Rayleigh * unit_rvec * t_init  # minus * minus
+
+
+
+
+
+
+
navier_stokes.solve(timestep=10.0)
+
+with meshball.access():
+    v_inertial = v_soln.data.copy()
+
+with swarm.access(v_star, remeshed, X_0):
+    v_star.data[...] = uw.function.evaluate(v_soln.fn, swarm.data)
+    X_0.data[...] = swarm.data[...]
+
+
+
+
+
+
+
swarm.advection(v_soln.fn, delta_t=navier_stokes.estimate_dt(), corrector=False)
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+
+
+def plot_V_mesh(filename):
+
+    if uw.mpi.size == 1:
+
+        import pyvista as pv
+        import underworld3.visualisation as vis
+
+        pvmesh = vis.mesh_to_pv_mesh(meshball)
+        pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+        
+        velocity_points = vis.meshVariable_to_pv_cloud(navier_stokes.u)
+        velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, navier_stokes.u.sym)
+
+        pl = pv.Plotter(window_size=(1000, 750))
+        pl.camera.SetPosition(0.0001, 0.0001, 4.0)
+
+        # pl.add_mesh(pvmesh,'Black', 'wireframe')
+        pl.add_mesh(
+            pvmesh,
+            cmap="coolwarm",
+            edge_color="Black",
+            show_edges=True,
+            use_transparency=False,
+            opacity=0.5,
+        )
+        pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.05)
+
+        pl.screenshot(
+            filename="{}.png".format(filename),
+            window_size=(2560, 2560),
+            return_img=False,
+        )
+
+        pl.close()
+
+        del pl
+
+
+
+
+
+
+
ts = 0
+swarm_loop = 5
+
+
+
+
+
+
+

+for step in range(0, 50):
+
+    Omega = 10.0 * meshball.N.k * min(ts / 25, 1.0)
+    navier_stokes.bodyforce = Rayleigh * unit_rvec * t_init  # minus * minus
+    navier_stokes.bodyforce -= (
+        2.0 * navier_stokes.rho * sympy.vector.cross(Omega, v_theta)
+    )
+
+    delta_t = 10.0 * navier_stokes.estimate_dt()
+
+    navier_stokes.solve(timestep=delta_t, zero_init_guess=False)
+
+    dv_fn = v_soln.fn - v_soln_1.fn
+    _, _, _, _, _, _, deltaV = meshball.stats(dv_fn.dot(dv_fn))
+
+    with meshball.access(v_soln_1):
+        v_soln_1.data[...] = 0.5 * v_soln_1.data[...] + 0.5 * v_soln.data[...]
+
+    with swarm.access(v_star):
+        v_star.data[...] = uw.function.evaluate(v_soln.fn, swarm.data)
+
+    swarm.advection(v_soln.fn, delta_t=delta_t, corrector=False)
+
+    # Restore a subset of points to start
+    offset_idx = step % swarm_loop
+
+    with swarm.access(swarm.particle_coordinates, remeshed):
+        remeshed.data[...] = 0
+        remeshed.data[offset_idx::swarm_loop, :] = 1
+        swarm.data[offset_idx::swarm_loop, :] = X_0.data[offset_idx::swarm_loop, :]
+
+    # re-calculate v history for remeshed particles
+    # Note, they may have moved procs after the access manager closed
+    # so we re-index
+
+    with swarm.access(v_star, remeshed):
+        idx = np.where(remeshed.data == 1)[0]
+        v_star.data[idx] = uw.function.evaluate(v_soln.fn, swarm.data[idx])
+
+    if uw.mpi.rank == 0:
+        print(
+            "Iteration (pseudo timestep) {}, dt {}, deltaV {}".format(
+                ts, delta_t, deltaV
+            )
+        )
+
+    if ts % 1 == 0:
+        # nodal_vorticity_from_v.solve()
+        plot_V_mesh(filename="output/{}_step_{}".format(expt_name, ts))
+
+        # savefile = "output/{}_ts_{}.h5".format(expt_name,step)
+        # meshball.save(savefile)
+        # v_soln.save(savefile)
+        # p_soln.save(savefile)
+        # vorticity.save(savefile)
+        # meshball.generate_xdmf(savefile)
+
+    navier_stokes._u_star_projector.smoothing = navier_stokes.viscosity * 1.0e-6
+
+    ts += 1
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+
+
+if uw.mpi.size == 1:
+
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshball)
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+    pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym)
+
+    velocity_points = vis.meshVariable_to_pv_cloud(navier_stokes.u)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, navier_stokes.u.sym)
+
+    pl = pv.Plotter(window_size=(1000, 750))
+
+    # pl.add_mesh(pvmesh,'Black', 'wireframe')
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        scalars="P",
+        use_transparency=False,
+        opacity=0.5,
+    )
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.033)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
((v_inertial - usol) ** 2).mean()
+
+
+
+
+
+
+
v_inertial.max()
+
+
+
+
+
+
+

#

+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Visualise_NS_DFG_2d.html b/main/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Visualise_NS_DFG_2d.html new file mode 100644 index 0000000..733a3b4 --- /dev/null +++ b/main/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Visualise_NS_DFG_2d.html @@ -0,0 +1,691 @@ + + + + + + + + + + + Navier Stokes test: flow around a circular inclusion (2D) — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Navier Stokes test: flow around a circular inclusion (2D)

+ +
+
+ +
+
+
+ + + + +
+ + + + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-NavierStokes/Readme.html b/main/Notebooks/Examples-NavierStokes/Readme.html new file mode 100644 index 0000000..2a17262 --- /dev/null +++ b/main/Notebooks/Examples-NavierStokes/Readme.html @@ -0,0 +1,589 @@ + + + + + + + + + + + Navier Stokes Equation — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Navier Stokes Equation

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ + + + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-NavierStokes/output/README.html b/main/Notebooks/Examples-NavierStokes/output/README.html new file mode 100644 index 0000000..38d0e78 --- /dev/null +++ b/main/Notebooks/Examples-NavierStokes/output/README.html @@ -0,0 +1,519 @@ + + + + + + + + + + + Navier-Stokes model outputs — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Navier-Stokes model outputs

+ +
+
+ +
+
+
+ + + + +
+ + + + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-PoissonEquation/Ex_DiffusionSLCN_Cartesian-NL.html b/main/Notebooks/Examples-PoissonEquation/Ex_DiffusionSLCN_Cartesian-NL.html new file mode 100644 index 0000000..8bba27a --- /dev/null +++ b/main/Notebooks/Examples-PoissonEquation/Ex_DiffusionSLCN_Cartesian-NL.html @@ -0,0 +1,1075 @@ + + + + + + + + + + + Nonlinear diffusion of a hot pipe — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Nonlinear diffusion of a hot pipe

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

Nonlinear diffusion of a hot pipe#

+
    +
  • Using the adv_diff solver.

  • +
  • No advection as the velocity field is not updated (and set to 0).

  • +
  • Comparison between 1D numerical solution and 2D UW model.

  • +
+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
from petsc4py import PETSc
+import underworld3 as uw
+from underworld3.systems import Stokes
+import numpy as np
+import sympy
+from mpi4py import MPI
+
+
+
+
+
+
+
import math
+
+
+
+
+
+
+
if uw.mpi.size == 1:
+    import matplotlib.pyplot as plt
+
+
+
+
+
+
+
# %%
+sys = PETSc.Sys()
+sys.pushErrorHandler("traceback")
+
+
+
+
+
+
+
# %%
+### Set the resolution.
+res = 32
+
+
+
+
+
+
+
xmin, xmax = 0.0, 1.0
+ymin, ymax = 0.0, 1.0
+
+
+
+
+
+
+
pipe_thickness = 0.4  ###
+
+
+
+
+
+
+
# %%
+k0 = 1e-6  ### m2/s (diffusivity)
+l0 = 1e5  ### 100 km in m (length of box)
+time_scale = l0**2 / k0  ### s
+time_scale_Myr = time_scale / (60 * 60 * 24 * 365.25 * 1e6)
+
+
+
+
+
+
+
mesh = uw.meshing.UnstructuredSimplexBox(
+    minCoords=(xmin, ymin), maxCoords=(xmax, ymax), cellSize=1.0 / res, regular=True
+)
+
+
+
+
+
+
+
# mesh = uw.meshing.StructuredQuadBox(
+#     elementRes=(int(res), int(res)), minCoords=(xmin, ymin), maxCoords=(xmax, ymax)
+# )
+
+
+
+
+

Create adv_diff object

+
+
+
# Set some things
+k = 1.0
+
+
+
+
+
+
+
tmin = 0.5
+tmax = 1.0
+
+
+
+
+
+
+
# Create an adv
+v = uw.discretisation.MeshVariable("U", mesh, mesh.dim, degree=2)
+T = uw.discretisation.MeshVariable("T", mesh, 1, degree=1)
+k = uw.discretisation.MeshVariable("k", mesh, 1, degree=1)
+
+
+
+
+
+
+
dTdY = uw.discretisation.MeshVariable(
+    r"\partial T/ \partial \mathbf{y}", mesh, 1, degree=2
+)
+
+
+
+
+
+
+
adv_diff = uw.systems.AdvDiffusionSLCN(
+                                        mesh,
+                                        u_Field=T,
+                                        V_fn=v,
+                                        solver_name="adv_diff",
+                                    )
+
+
+
+
+
+
+
adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel
+
+
+
+
+
+
+
# %%
+delT = mesh.vector.gradient(T.sym)
+gradient = delT.dot(delT)
+
+
+
+
+
+
+
k_sym = (delT.dot(delT)) / 2.0
+
+
+
+
+
+
+
adv_diff.constitutive_model.Parameters.diffusivity = k_sym
+
+
+
+
+
+
+
# %%
+k_model = uw.systems.Projection(mesh, k)
+k_model.uw_function = adv_diff.constitutive_model.Parameters.diffusivity
+k_model.smoothing = 1.0e-3
+### set diffusivity BCs
+# k_model.add_dirichlet_bc(0., ["Top", "Bottom"], components=0)
+
+
+
+
+
+
+
def updateFields():
+    k_model.uw_function = adv_diff.constitutive_model.Parameters.diffusivity
+    k_model.solve(_force_setup=True)
+
+
+
+
+
+
+
### fix temp of top and bottom walls
+adv_diff.add_dirichlet_bc(0.5, "Bottom", 0)
+adv_diff.add_dirichlet_bc(0.5, "Top", 0)
+
+
+
+
+
+
+
maxY = mesh.data[:, 1].max()
+minY = mesh.data[:, 1].min()
+
+
+
+
+
+
+
with mesh.access(T):
+    T.data[...] = tmin
+
+    pipePosition = ((maxY - minY) - pipe_thickness) / 2.0
+
+    T.data[
+        (mesh.data[:, 1] >= (mesh.data[:, 1].min() + pipePosition))
+        & (mesh.data[:, 1] <= (mesh.data[:, 1].max() - pipePosition))
+    ] = tmax
+
+
+
+
+
+
+
def plot_fig():
+    updateFields()
+
+    if uw.mpi.size == 1:
+        
+        import pyvista as pv
+        import underworld3.visualisation as vis
+
+        pvmesh = vis.mesh_to_pv_mesh(mesh)
+        pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, T.sym)
+        pvmesh.point_data["k"] = vis.scalar_fn_to_pv_points(pvmesh, k.sym)
+        
+        velocity_points = vis.meshVariable_to_pv_cloud(v)
+        velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v.sym)
+
+        pl = pv.Plotter(window_size=(750, 750))
+
+        pl.add_mesh(pvmesh, "Black", "wireframe")
+
+        # pvmesh.point_data["rho"] = uw.function.evaluate(density, mesh.data)
+
+        pl.add_mesh(
+            pvmesh,
+            cmap="coolwarm",
+            edge_color="Black",
+            show_edges=True,
+            scalars="k",
+            use_transparency=False,
+            opacity=0.95,
+        )
+
+        # pl.add_mesh(pvmesh, cmap="coolwarm", edge_color="Black", show_edges=True, scalars="S",
+        #               use_transparency=False, opacity=0.5)
+
+        # pl.add_mesh(
+        #     point_cloud,
+        #     cmap="coolwarm",
+        #     edge_color="Black",
+        #     show_edges=False,
+        #     scalars="M",
+        #     use_transparency=False,
+        #     opacity=0.95,
+        # )
+
+        pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=5.0, opacity=0.5)
+        # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1)
+
+        # pl.add_points(pdata)
+
+        pl.show(cpos="xy")
+
+        # return vsol
+
+
+
+
+
+
+
plot_fig()
+
+
+
+
+
+

Vertical profile across the centre of the box#

+
+
+
### y coords to sample
+sample_y = np.arange(
+    mesh.data[:, 1].min(), mesh.data[:, 1].max(), mesh.get_min_radius()
+)  ### Vertical profile
+
+
+
+
+
+
+
### x coords to sample
+# sample_x = np.repeat(mesh.data[:,0].min(), sample_y.shape[0]) ### LHS wall
+sample_x = np.zeros_like(sample_y)  ### centre of the box
+
+
+
+
+
+
+
sample_points = np.empty((sample_x.shape[0], 2))
+sample_points[:, 0] = sample_x
+sample_points[:, 1] = sample_y
+
+
+
+
+
+
+
t0 = uw.function.evaluate(adv_diff.u.fn, sample_points)
+
+
+
+
+
+
+
def get_dt():
+    updateFields()
+    with mesh.access(k):
+        ### estimate the timestep based on diffusion only
+        dt = (
+            mesh.get_min_radius() ** 2 / k.data[:, 0].max()
+        )  ### dt = length squared / diffusivity
+
+    # print(f'dt: {dt*time_scale_Myr} Myr')
+    print(f"dt: {dt*time_scale_Myr}")
+
+    return dt
+
+
+
+
+
+
+
def diffusion_1D(sample_points, tempProfile, k, model_dt):
+    x = sample_points
+    T = tempProfile
+
+    dx = sample_points[1] - sample_points[0]
+
+    dt = 0.5 * (dx**2 / k)
+
+    """ max time of model """
+    total_time = model_dt
+
+    """ get min of 1D and 2D model """
+    time_1DModel = min(model_dt, dt)
+
+    """ determine number of its """
+    nts = math.ceil(total_time / time_1DModel)
+
+    """ get dt of 1D model """
+    final_dt = total_time / nts
+
+    for i in range(nts):
+        qT = -k * np.diff(T) / dx
+        dTdt = -np.diff(qT) / dx
+        T[1:-1] += dTdt * final_dt
+
+    return T
+
+
+
+
+
+
+
### get the initial temp profile
+tempData = uw.function.evaluate(adv_diff.u.fn, sample_points)
+
+
+
+
+
+
+
step = 0
+time = 0.0
+
+
+
+
+
+
+
nsteps = 1 # 21
+
+
+
+
+
+
+
adv_diff.petsc_options["ksp_rtol"] = 1.0e-8
+
+
+
+
+
+
+
adv_diff.petsc_options["snes_rtol"] = 1.0e-8
+
+
+
+
+
+
+
# if uw.mpi.size == 1:
+#     ''' create figure to show the temp diffuses '''
+#     plt.figure(figsize=(9, 3))
+#     plt.plot(t0, sample_points[:,1], ls=':')
+
+
+
+
+
+
+
while step < nsteps:
+    ### print some stuff
+    if uw.mpi.rank == 0:
+        # print(f"Step: {str(step).rjust(3)}, time: {time*time_scale_Myr:6.2f} [MYr]")
+        print(f"Step: {str(step).rjust(3)}, time: {time:6.5f}")
+
+    ### 1D profile from underworld
+    t1 = uw.function.evaluate(adv_diff.u.fn, sample_points)
+
+    if uw.mpi.size == 1 and step % 10 == 0:
+        """compare 1D and 2D models"""
+        plt.figure()
+        ### profile from UW
+        plt.plot(t1, sample_points[:, 1], ls="-", c="red", label="2D nonlinear model")
+        ### numerical solution
+        plt.plot(tempData, sample_points[:, 1], ls=":", c="k", label="1D linear model")
+        plt.legend()
+        plt.show()
+
+    dt = get_dt()
+
+    ### 1D diffusion
+    tempData = diffusion_1D(
+        sample_points=sample_points[:, 1], tempProfile=tempData, k=1.0, model_dt=dt
+    )
+
+    ### diffuse through underworld
+    adv_diff.solve(timestep=dt)
+
+    step += 1
+    time += dt
+
+
+
+
+
+
+
plt.show()
+
+
+
+
+
+
+
plot_fig()
+
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-PoissonEquation/Ex_DiffusionSLCN_Cartesian.html b/main/Notebooks/Examples-PoissonEquation/Ex_DiffusionSLCN_Cartesian.html new file mode 100644 index 0000000..717086c --- /dev/null +++ b/main/Notebooks/Examples-PoissonEquation/Ex_DiffusionSLCN_Cartesian.html @@ -0,0 +1,982 @@ + + + + + + + + + + + Linear diffusion of a hot pipe — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Linear diffusion of a hot pipe

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

Linear diffusion of a hot pipe#

+
    +
  • Using the adv_diff solver.

  • +
  • No advection as the velocity field is not updated (and set to 0).

  • +
  • Benchmark comparison between 1D numerical solution and 2D UW model.

  • +
+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
# %%
+from petsc4py import PETSc
+import underworld3 as uw
+from underworld3.systems import Stokes
+import numpy as np
+import sympy
+from mpi4py import MPI
+
+
+
+
+
+
+
import math
+
+
+
+
+
+
+
if uw.mpi.size == 1:
+    import matplotlib.pyplot as plt
+
+
+
+
+
+
+
# %%
+sys = PETSc.Sys()
+sys.pushErrorHandler("traceback")
+
+
+
+
+
+
+
# %%
+# Set the resolution.
+res = 32
+
+
+
+
+
+
+
xmin, xmax = 0.0, 1.0
+ymin, ymax = 0.0, 1.0
+
+
+
+
+
+
+
pipe_thickness = 0.4  ###
+
+
+
+
+
+
+
# %%
+k0 = 1e-6  ### m2/s (diffusivity)
+l0 = 1e5  ### 100 km in m (length of box)
+time_scale = l0**2 / k0  ### s
+time_scale_Myr = time_scale / (60 * 60 * 24 * 365.25 * 1e6)
+
+
+
+
+

mesh = uw.meshing.UnstructuredSimplexBox(minCoords=(xmin, ymin), maxCoords=(xmax, ymax), cellSize=1.0 / res, regular=True)

+
+
+
mesh = uw.meshing.StructuredQuadBox(
+    elementRes=(int(res), int(res)), minCoords=(xmin, ymin), maxCoords=(xmax, ymax)
+)
+
+
+
+
+

Create adv_diff object

+
+
+
# Set some things
+k = 1.0
+
+
+
+
+
+
+
tmin = 0.5
+tmax = 1.0
+
+
+
+
+
+
+
# Create an adv
+v = uw.discretisation.MeshVariable("U", mesh, mesh.dim, degree=2)
+T = uw.discretisation.MeshVariable("T", mesh, 1, degree=1)
+
+
+
+
+
+
+
adv_diff = uw.systems.AdvDiffusionSLCN(
+    mesh,
+    u_Field=T,
+    V_fn=v,
+    solver_name="adv_diff",
+)
+
+
+
+
+
+
+
adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel
+adv_diff.constitutive_model.Parameters.diffusivity = k
+
+
+
+
+
+
+
# %%
+### fix temp of top and bottom walls
+adv_diff.add_dirichlet_bc(0.5, "Bottom", 0)
+adv_diff.add_dirichlet_bc(0.5, "Top", 0)
+
+
+
+
+
+
+
# %%
+maxY = mesh.data[:, 1].max()
+minY = mesh.data[:, 1].min()
+
+
+
+
+
+
+
with mesh.access(T):
+    T.data[...] = tmin
+
+    pipePosition = ((maxY - minY) - pipe_thickness) / 2.0
+
+    T.data[
+        (mesh.data[:, 1] >= (mesh.data[:, 1].min() + pipePosition))
+        & (mesh.data[:, 1] <= (mesh.data[:, 1].max() - pipePosition))
+    ] = tmax
+
+
+
+
+
+
+
# %%
+def plot_fig():
+    if uw.mpi.size == 1:
+        
+        import pyvista as pv
+        import underworld3.visualisation as vis
+
+        pvmesh = vis.mesh_to_pv_mesh(mesh)
+        pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, T.sym)
+        
+        velocity_points = vis.meshVariable_to_pv_cloud(v)
+        velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v.sym)
+
+        pl = pv.Plotter(window_size=(750, 750))
+
+        pl.add_mesh(pvmesh, "Black", "wireframe")
+
+        # pvmesh.point_data["rho"] = uw.function.evaluate(density, mesh.data)
+
+        pl.add_mesh(
+            pvmesh,
+            cmap="coolwarm",
+            edge_color="Black",
+            show_edges=True,
+            scalars="T",
+            use_transparency=False,
+            opacity=0.95,
+        )
+
+        # pl.add_mesh(pvmesh, cmap="coolwarm", edge_color="Black", show_edges=True, scalars="S",
+        #               use_transparency=False, opacity=0.5)
+
+        # pl.add_mesh(
+        #     point_cloud,
+        #     cmap="coolwarm",
+        #     edge_color="Black",
+        #     show_edges=False,
+        #     scalars="M",
+        #     use_transparency=False,
+        #     opacity=0.95,
+        # )
+
+        pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=5.0, opacity=0.5)
+        # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1)
+
+        # pl.add_points(pdata)
+
+        pl.show(cpos="xy")
+
+        # return vsol
+
+
+
+
+
+
+
plot_fig()
+
+
+
+
+
+

Vertical profile across the centre of the box#

+
+
+
### y coords to sample
+sample_y = np.arange(
+    mesh.data[:, 1].min(), mesh.data[:, 1].max(), mesh.get_min_radius()
+)  ### Vertical profile
+
+
+
+
+
+
+
### x coords to sample
+# sample_x = np.repeat(mesh.data[:,0].min(), sample_y.shape[0]) ### LHS wall
+sample_x = np.zeros_like(sample_y)  ### centre of the box
+
+
+
+
+
+
+
sample_points = np.empty((sample_x.shape[0], 2))
+sample_points[:, 0] = sample_x
+sample_points[:, 1] = sample_y
+
+
+
+
+
+
+
t0 = uw.function.evaluate(adv_diff.u.fn, sample_points)
+
+
+
+
+
+
+
# %%
+### estimate the timestep based on diffusion only
+dt = mesh.get_min_radius() ** 2 / k  ### dt = length squared / diffusivity
+# print(f'dt: {dt*time_scale_Myr} Myr')
+print(f"dt: {dt*time_scale_Myr}")
+
+
+
+
+
+
+
# %%
+def diffusion_1D(sample_points, tempProfile, k, model_dt):
+    x = sample_points
+    T = tempProfile
+
+    dx = sample_points[1] - sample_points[0]
+
+    dt = 0.5 * (dx**2 / k)
+
+    """ max time of model """
+    total_time = model_dt
+
+    """ get min of 1D and 2D model """
+    time_1DModel = min(model_dt, dt)
+
+    """ determine number of its """
+    nts = math.ceil(total_time / time_1DModel)
+
+    """ get dt of 1D model """
+    final_dt = total_time / nts
+
+    for i in range(nts):
+        qT = -k * np.diff(T) / dx
+        dTdt = -np.diff(qT) / dx
+        T[1:-1] += dTdt * final_dt
+
+    return T
+
+
+
+
+
+
+
### get the initial temp profile
+tempData = uw.function.evaluate(adv_diff.u.fn, sample_points)
+
+
+
+
+
+
+

+step = 0
+time = 0.0
+
+
+
+
+
+
+
# %%
+nsteps = 1 # 21
+
+
+
+
+
+
+

+if uw.mpi.size == 1:
+    """create figure to show the temp diffuses"""
+    plt.figure(figsize=(9, 3))
+    plt.plot(t0, sample_points[:, 1], ls=":")
+
+
+
+
+
+
+
while step < nsteps:
+    ### print some stuff
+    if uw.mpi.rank == 0:
+        # print(f"Step: {str(step).rjust(3)}, time: {time*time_scale_Myr:6.2f} [MYr]")
+        print(f"Step: {str(step).rjust(3)}, time: {time:6.5f}")
+
+    ### 1D profile from underworld
+    t1 = uw.function.evalf(adv_diff.u.sym[0], sample_points)
+
+    if uw.mpi.size == 1 and step % 10 == 0:
+        """compare 1D and 2D models"""
+        plt.figure()
+        ### profile from UW
+        plt.plot(t1, sample_points[:, 1], ls="-", c="red", label="2D linear model")
+        ### numerical solution
+        plt.plot(tempData, sample_points[:, 1], ls=":", c="k", label="1D linear model")
+        plt.legend()
+        plt.show()
+
+    ### 1D diffusion
+    tempData = diffusion_1D(
+        sample_points=sample_points[:, 1], tempProfile=tempData, k=k, model_dt=dt
+    )
+
+    ### diffuse through underworld
+    adv_diff.solve(timestep=dt)
+
+    step += 1
+    time += dt
+
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-PoissonEquation/Ex_Poisson_Cartesian.html b/main/Notebooks/Examples-PoissonEquation/Ex_Poisson_Cartesian.html new file mode 100644 index 0000000..60e32c9 --- /dev/null +++ b/main/Notebooks/Examples-PoissonEquation/Ex_Poisson_Cartesian.html @@ -0,0 +1,1116 @@ + + + + + + + + + + + Poisson Equation (simple) — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Poisson Equation (simple)

+ +
+ +
+
+ + + + +
+ +
+

Poisson Equation (simple)#

+

First we show how this works using the generic class and then the minor differences for +the Poisson class

+
+

Generic scalar solver class#

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
from petsc4py import PETSc
+
+import os
+
+os.environ["UW_TIMING_ENABLE"] = "1"
+
+import underworld3 as uw
+from underworld3 import timing
+
+import numpy as np
+import sympy
+
+from IPython.display import display
+
+
+
+
+
+
+
mesh1 = uw.meshing.UnstructuredSimplexBox(
+    minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0), cellSize=1.0 / 4, refinement=4
+)
+
+mesh2 = uw.meshing.UnstructuredSimplexBox(
+    minCoords=(0.0, 0.0),
+    maxCoords=(1.0, 1.0),
+    cellSize=1.0 / 4,
+    regular=True,
+    refinement=4,
+)
+
+
+
+
+
+
+
# pick a mesh
+mesh = mesh1
+
+
+
+
+
+
+
phi = uw.discretisation.MeshVariable("Phi", mesh, 1, degree=2, varsymbol=r"\phi")
+scalar = uw.discretisation.MeshVariable(
+    "Theta", mesh, 1, degree=1, continuous=False, varsymbol=r"\Theta"
+)
+
+
+
+
+

Create Poisson object

+
+
+
poisson = uw.systems.Poisson(mesh, u_Field=phi, solver_name="diffusion")
+
+
+
+
+

Constitutive law (diffusivity)

+
+
+
poisson.constitutive_model = uw.constitutive_models.DiffusionModel
+poisson.constitutive_model.Parameters.diffusivity = 1
+
+
+
+
+
+
+
# %%
+poisson.constitutive_model.c
+
+
+
+
+
+
+
# Set some things
+poisson.f = 0.0
+poisson.add_dirichlet_bc(1.0, "Bottom", components=0)
+poisson.add_dirichlet_bc(0.0, "Top", components=0)
+
+poisson.tolerance = 1.0e-6
+poisson.petsc_options["snes_type"] = "newtonls"
+poisson.petsc_options["ksp_type"] = "fgmres"
+
+poisson.petsc_options["snes_monitor"] = None
+poisson.petsc_options["ksp_monitor"] = None
+poisson.petsc_options.setValue("pc_type", "mg")
+poisson.petsc_options.setValue("pc_mg_type", "multiplicative")
+poisson.petsc_options.setValue("pc_mg_type", "kaskade")
+# poisson.petsc_options["mg_levels"] = mesh.dm.getRefineLevel()-2
+poisson.petsc_options["mg_levels_ksp_type"] = "fgmres"
+poisson.petsc_options["mg_levels_ksp_max_it"] = 100
+poisson.petsc_options["mg_levels_ksp_converged_maxits"] = None
+poisson.petsc_options["mg_coarse_pc_type"] = "svd"
+
+
+
+
+
+
+
poisson.view()
+
+
+
+
+
+
+
poisson._setup_pointwise_functions(verbose=True)
+
+
+
+
+
+
+
poisson._setup_discretisation()
+
+
+
+
+
+
+
timing.reset()
+timing.start()
+
+
+
+
+
+
+
# %%
+# Solve time
+poisson.solve()
+
+
+
+
+
+
+
type(poisson.F1)
+
+
+
+
+
+
+
# %%
+# Check. Construct simple linear function which is solution for
+# above config.  Exclude boundaries from mesh data.
+import numpy as np
+
+
+
+
+
+
+
with mesh.access():
+    mesh_numerical_soln = uw.function.evalf(poisson.u.fn, mesh.data)
+    mesh_analytic_soln = uw.function.evalf(1.0 - mesh.N.y, mesh.data)
+    if not np.allclose(mesh_analytic_soln, mesh_numerical_soln, rtol=0.0001):
+        print("Unexpected values encountered.")
+
+
+
+
+

Validate

+
+
+
if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh)
+
+    pvmesh.point_data["T"] = mesh_analytic_soln
+    pvmesh.point_data["T2"] = mesh_numerical_soln
+    pvmesh.point_data["DT"] = pvmesh.point_data["T"] - pvmesh.point_data["T2"]
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        scalars="DT",
+        use_transparency=False,
+        opacity=0.5,
+        # scalar_bar_args=sargs,
+    )
+
+    pl.camera_position = "xy"
+
+    pl.show(cpos="xy")
+    # pl.screenshot(filename="test.png")
+
+
+
+
+

Create some arbitrary function using one of the base scalars x,y[,z] = mesh.X

+
+
+
import sympy
+
+
+
+
+
+
+
x, y = mesh.X
+x0 = y0 = 1 / sympy.sympify(2)
+k = sympy.exp(-((x - x0) ** 2 + (y - y0) ** 2))
+
+
+
+
+
+
+
poisson.constitutive_model.Parameters.diffusivity = k
+
+
+
+
+
+
+
poisson.constitutive_model.flux
+
+
+
+
+
+
+
with mesh.access():
+    orig_soln = poisson.u.data.copy()
+
+
+
+
+
+
+
orig_soln_mesh = uw.function.evalf(phi.sym[0], mesh.data)
+
+
+
+
+
+
+
# %%
+poisson.solve(zero_init_guess=True, _force_setup=True)
+
+
+
+
+
+
+
print(poisson.Unknowns.u.stats())
+
+
+
+
+

Simply confirm results are different

+
+
+
with mesh.access():
+    if np.allclose(poisson.u.data, orig_soln, rtol=0.001):
+        raise RuntimeError("Values did not change !")
+
+
+
+
+
+
+
mesh._evaluation_hash = None
+
+
+
+
+

Visual validation

+
+
+
if uw.mpi.size == 1:
+   
+    import pyvista as pv
+    import underworld3.visualisation as vis
+   
+    pvmesh2 = vis.mesh_to_pv_mesh(mesh)
+    
+    pvmesh2.point_data["T"] = uw.function.evaluate(phi.sym[0], mesh.data)
+    pvmesh2.point_data["Te"] = uw.function.evalf(phi.sym[0], mesh.data)
+    pvmesh2.point_data["dT"] = pvmesh2.point_data["T"] - pvmesh.point_data["T"]
+    pvmesh2.point_data["dTe"] = pvmesh2.point_data["T"] - pvmesh2.point_data["Te"]
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_mesh(
+        pvmesh2,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        scalars="dT",
+        use_transparency=False,
+        opacity=0.5,
+        # scalar_bar_args=sargs,
+    )
+
+    pl.camera_position = "xy"
+
+    pl.show(cpos="xy")
+    # pl.screenshot(filename="test.png")
+
+
+
+
+
+
+

Non-linear example#

+

RHS term

+
+
+
abs_r2 = x**2 + y**2
+poisson.f = -16 * abs_r2
+poisson.add_dirichlet_bc(abs_r2, "Bottom", components=0)
+poisson.add_dirichlet_bc(abs_r2, "Top", components=0)
+poisson.add_dirichlet_bc(abs_r2, "Right", components=0)
+poisson.add_dirichlet_bc(abs_r2, "Left", components=0)
+
+
+
+
+
+
+
display(poisson.f)
+
+
+
+
+

Constitutive law (diffusivity) +Linear solver first

+
+
+
poisson.constitutive_model = uw.constitutive_models.DiffusionModel
+poisson.constitutive_model.Parameters.diffusivity = 1
+
+
+
+
+
+
+
poisson.solve()
+
+
+
+
+

Non-linear diffusivity

+
+
+
grad_phi = mesh.vector.gradient(phi.sym)
+k = 5 + (grad_phi.dot(grad_phi)) / 2
+poisson.constitutive_model.Parameters.diffusivity = k
+poisson.constitutive_model.c
+
+
+
+
+
+
+
# %%
+poisson._setup_pointwise_functions()
+poisson._G3
+
+
+
+
+

Use initial guess from linear solve

+
+
+
poisson.solve(zero_init_guess=False)
+
+
+
+
+

Validate

+
+
+
if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+    
+    pvmesh2 = vis.mesh_to_pv_mesh(mesh)
+    pvmesh2.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh2, phi.sym)
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_mesh(
+        pvmesh2,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        scalars="T",
+        use_transparency=False,
+        opacity=0.5,
+        # scalar_bar_args=sargs,
+    )
+
+    pl.camera_position = "xy"
+
+    pl.show(cpos="xy")
+    # pl.screenshot(filename="test.png")
+
+
+
+
+
+
+

Analysis (Gradient recovery)#

+

We’d like to be able to look at the values of diffusivity or the +heat flux.

+

These are discontinuous values computed in the element interiors but can +be projected to a meshVariable:

+
+
+
# %%
+projection = uw.systems.Projection(mesh, scalar)
+projection.uw_function = sympy.diff(phi.sym[0], mesh.X[1])
+projection.smoothing = 1.0e-4
+
+
+
+
+
+
+
projection.solve()
+
+
+
+
+
+
+
with mesh.access():
+    print(phi.stats())
+    print(scalar.stats())
+
+
+
+
+
+
+
# %%
+sympy.diff(scalar.sym[0], mesh.X[1])
+
+
+
+
+

Validate

+
+
+
if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+    
+    pvmesh2 = vis.mesh_to_pv_mesh(mesh)
+
+    pvmesh2.point_data["K"] = vis.scalar_fn_to_pv_points(pvmesh2, scalar.sym)
+    pvmesh2.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh2, phi.sym)
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_mesh(
+        pvmesh2,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        scalars="T",
+        use_transparency=False,
+        opacity=0.5,
+        # scalar_bar_args=sargs,
+    )
+
+    pl.camera_position = "xy"
+
+    pl.show(cpos="xy")
+    # pl.screenshot(filename="test.png")
+
+
+
+
+
+
+
poisson.snes.view()
+
+
+
+
+
+
+
timing.print_table()
+
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-PoissonEquation/Ex_Poisson_Cartesian_Generic.html b/main/Notebooks/Examples-PoissonEquation/Ex_Poisson_Cartesian_Generic.html new file mode 100644 index 0000000..a1bb5fb --- /dev/null +++ b/main/Notebooks/Examples-PoissonEquation/Ex_Poisson_Cartesian_Generic.html @@ -0,0 +1,782 @@ + + + + + + + + + + + Poisson Equation (generic) — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Poisson Equation (generic)

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

Poisson Equation (generic)#

+

First we show how this works using the generic class and then the minor differences for +the Poisson class

+
+

Generic scalar solver class#

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import underworld3 as uw
+import numpy as np
+import sympy
+
+
+
+
+
+
+
from underworld3.meshing import UnstructuredSimplexBox
+
+
+
+
+
+
+
mesh = UnstructuredSimplexBox(
+    minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0), regular=False, cellSize=1.0 / 32
+)
+
+
+
+
+
+
+
t_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=1)
+t_soln0 = uw.discretisation.MeshVariable("T0", mesh, 1, degree=1)
+
+
+
+
+
+
+
poisson0 = uw.systems.SNES_Scalar(mesh, u_Field=t_soln0)
+poisson0.F0 = 0.0
+poisson0.F1 = 1.0 * poisson0.Unknowns.L
+poisson0.add_dirichlet_bc(1.0, "Bottom", 0)
+poisson0.add_dirichlet_bc(0.0, "Top", 0)
+
+poisson0.constitutive_model = uw.constitutive_models.DiffusionModel
+poisson0.constitutive_model.Parameters.diffusivity = 1.0
+
+
+
+
+
+
+
poisson0.solve()
+
+
+
+
+
+
+

Poisson Class#

+

Here is the other way to solve this, using the Poisson class which does not much +more than add a template for the flux term.

+
+
+
# Create Poisson object
+poisson = uw.systems.Poisson(mesh, u_Field=t_soln)
+poisson.constitutive_model = uw.constitutive_models.DiffusionModel
+poisson.constitutive_model.Parameters.diffusivity = 1.0
+
+poisson.f = 0.0
+poisson.add_dirichlet_bc(1.0, "Bottom", 0)
+poisson.add_dirichlet_bc(0.0, "Top", 0)
+
+
+
+
+
+
+
# Solve time
+poisson.solve()
+
+
+
+
+
+
+
poisson.F0
+sympy.Matrix((0,))
+
+
+
+
+
+
+
# Check the flux term
+display(poisson._L)
+
+# This is the internal build of the flux term
+display(poisson._f1)
+
+
+
+
+
+
+
poisson.Unknowns.L
+
+
+
+
+
+
+
poisson.u.sym.jacobian(poisson.Unknowns.L)
+
+
+
+
+
+
+
poisson._f1.jacobian(poisson.Unknowns.L)
+
+
+
+
+
+
+

Validation#

+
+
+
# Check. Construct simple linear which is solution for
+# above config.  Exclude boundaries from mesh data.
+
+import numpy as np
+
+with mesh.access():
+    mesh_numerical_soln = uw.function.evaluate(poisson.u.fn, mesh.data)
+    mesh_analytic_soln = uw.function.evaluate(1.0 - mesh.N.y, mesh.data)
+
+    if not np.allclose(mesh_analytic_soln, mesh_numerical_soln, rtol=0.001, atol=0.01):
+        raise RuntimeError("Unexpected values encountered.")
+
+
+
+
+
+
+
from mpi4py import MPI
+
+if MPI.COMM_WORLD.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh)
+    pvmesh.point_data["T"] = mesh_analytic_soln
+    pvmesh.point_data["T2"] = mesh_numerical_soln
+    pvmesh.point_data["DT"] = pvmesh.point_data["T"] - pvmesh.point_data["T2"]
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        scalars="DT",
+        use_transparency=False,
+        opacity=0.5,
+    )
+
+    pl.camera_position = "xy"
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-PoissonEquation/Ex_Poisson_Gradient_Recovery.html b/main/Notebooks/Examples-PoissonEquation/Ex_Poisson_Gradient_Recovery.html new file mode 100644 index 0000000..40f6df9 --- /dev/null +++ b/main/Notebooks/Examples-PoissonEquation/Ex_Poisson_Gradient_Recovery.html @@ -0,0 +1,922 @@ + + + + + + + + + + + Poisson Equation with flux recovery — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Poisson Equation with flux recovery

+ +
+ +
+
+ + + + +
+ +
+

Poisson Equation with flux recovery#

+
+

Generic scalar solver class#

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
from petsc4py import PETSc
+import underworld3 as uw
+
+
+
+
+
+
+
import numpy as np
+import sympy
+
+
+
+
+
+
+
# %%
+mesh = uw.meshing.UnstructuredSimplexBox(
+    minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0), cellSize=1.0 / 3.0, qdegree=2
+)
+
+
+
+
+
+
+
mesh.dm.view()
+
+
+
+
+

mesh variables

+
+
+
t_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=3)
+dTdY = uw.discretisation.MeshVariable(
+    r"\partial T/ \partial \mathbf{y}", mesh, 1, degree=2
+)
+kappa = uw.discretisation.MeshVariable(r"\kappa", mesh, 1, degree=2)
+gradT = uw.discretisation.MeshVariable(
+    r"\nabla\left[T\right]", mesh, mesh.dim, degree=2
+)
+
+
+
+
+

Create Poisson object

+
+
+
gradient = uw.systems.Projection(mesh, dTdY)
+delT = mesh.vector.gradient(t_soln.sym)
+gradient.uw_function = delT.dot(delT)
+gradient.smoothing = 1.0e-3
+
+
+
+
+

These are both SNES Scalar objects

+
+
+
gradT_projector = uw.systems.Vector_Projection(mesh, gradT)
+gradT_projector.uw_function = mesh.vector.gradient(t_soln.sym)
+# gradT_projector.add_dirichlet_bc((0), ["Left", "Right"], components=(0))
+
+
+
+
+
+
+
+

the actual solver#

+
+
+
poisson = uw.systems.Poisson(mesh, u_Field=t_soln)
+
+
+
+
+
+
+
# %%
+poisson.constitutive_model = uw.constitutive_models.DiffusionModel
+
+
+
+
+

Non-linear diffusivity

+
+
+
delT = mesh.vector.gradient(t_soln.sym)
+k = 5 + (delT.dot(delT)) / 2
+
+
+
+
+
+
+
poisson.constitutive_model.Parameters.diffusivity = k
+display(poisson.constitutive_model.c)
+
+
+
+
+

projector for diffusivity (though we can just switch the rhs for the gradient object

+
+
+
diffusivity = uw.systems.Projection(mesh, kappa)
+diffusivity.uw_function = sympy.Matrix(
+    [poisson.constitutive_model.Parameters.diffusivity]
+)
+
+diffusivity.add_dirichlet_bc(k, "Bottom", components=0)
+diffusivity.add_dirichlet_bc(k, "Top", components=0)
+diffusivity.add_dirichlet_bc(k, "Right", components=0)
+diffusivity.add_dirichlet_bc(k, "Left", components=0)
+
+diffusivity.smoothing = 1.0e-6
+
+
+
+
+
+
+
# %%
+poisson.constitutive_model = uw.constitutive_models.DiffusionModel
+poisson.constitutive_model.Parameters.diffusivity = k
+poisson.constitutive_model.Parameters.diffusivity
+
+
+
+
+
+
+
# %%
+display(gradT_projector.uw_function)
+display(diffusivity.uw_function)
+
+
+
+
+
+
+
# %%
+diffusivity.uw_function
+
+
+
+
+

Set some things

+
+
+
x, y = mesh.X
+
+
+
+
+
+
+
abs_r2 = x**2 + y**2
+poisson.f = -16 * abs_r2
+poisson.add_dirichlet_bc(abs_r2, "Bottom", components=0)
+poisson.add_dirichlet_bc(abs_r2, "Top", components=0)
+poisson.add_dirichlet_bc(abs_r2, "Right", components=0)
+poisson.add_dirichlet_bc(abs_r2, "Left", components=0)
+
+
+
+
+
+
+
# %%
+# Linear model - starting guess
+
+poisson.constitutive_model.Parameters.diffusivity = 1
+poisson.solve(zero_init_guess=True)
+
+
+
+
+
+
+
# %%
+# Solve time
+poisson.constitutive_model.Parameters.diffusivity = k
+poisson.solve(zero_init_guess=False)
+
+
+
+
+
+
+
# %%
+poisson.constitutive_model
+
+
+
+
+
+
+
# %%
+gradT_projector.solve()
+
+
+
+
+
+
+
# %%
+gradient.uw_function = sympy.diff(t_soln.sym, mesh.N.y)
+gradient.solve()
+
+
+
+
+
+
+
# %%
+gradient.uw_function
+
+
+
+
+
+
+
# %%
+diffusivity.solve()
+
+
+
+
+

non-linear smoothing term (probably not needed especially at the boundary)

+
+
+
gradient.uw_function = sympy.diff(t_soln.fn, mesh.N.y)
+gradient.solve(_force_setup=True)
+
+
+
+
+
+
+
# %%
+gradT_projector.solve()
+
+
+
+
+

Check Construct simple linear function which is solution for +above config. Exclude boundaries from mesh data.

+
+
+
import numpy as np
+
+
+
+
+
+
+
with mesh.access():
+    mesh_numerical_soln = uw.function.evaluate(t_soln.sym[0], mesh.data)
+    # if not np.allclose(mesh_numerical_soln, -1.0, rtol=0.01):
+    #     raise RuntimeError("Unexpected values encountered.")
+
+
+
+
+

Validate

+
+
+
from mpi4py import MPI
+
+
+
+
+
+
+
if MPI.COMM_WORLD.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh)
+    pvmesh.point_data["T"] = mesh_numerical_soln
+    pvmesh.point_data["dTdY"] = vis.scalar_fn_to_pv_points(pvmesh, dTdY.sym)
+    pvmesh.point_data["dTdY1"] = vis.scalar_fn_to_pv_points(pvmesh, gradT.sym[1])
+    pvmesh.point_data["dTdX1"] = vis.scalar_fn_to_pv_points(pvmesh, gradT.sym[0])
+    pvmesh.point_data["kappa"] = vis.scalar_fn_to_pv_points(pvmesh, kappa.sym)
+    pvmesh.point_data["kappa1"] = vis.scalar_fn_to_pv_points(pvmesh, 5 + gradT.sym[0] ** 2 + gradT.sym[1] ** 2)
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        scalars="dTdX1",
+        use_transparency=False,
+        opacity=0.5,
+    )
+
+    pl.camera_position = "xy"
+
+    pl.show(cpos="xy")
+    # pl.screenshot(filename="test.png")
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-PoissonEquation/Ex_Poisson_Spherical.html b/main/Notebooks/Examples-PoissonEquation/Ex_Poisson_Spherical.html new file mode 100644 index 0000000..fce04da --- /dev/null +++ b/main/Notebooks/Examples-PoissonEquation/Ex_Poisson_Spherical.html @@ -0,0 +1,1019 @@ + + + + + + + + + + + Steady state diffusion in a hollow sphere — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Steady state diffusion in a hollow sphere

+ +
+
+ +
+
+
+ + + + +
+ +
+

Steady state diffusion in a hollow sphere#

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import pygmsh, meshio
+
+
+
+
+
+
+
from petsc4py import PETSc
+import underworld3 as uw
+from underworld3.systems import Poisson
+import numpy as np
+import os
+
+# Define the problem size
+#      1 - ultra low res for automatic checking
+#      2 - low res problem to play with this notebook
+#      3 - medium resolution (be prepared to wait)
+#      4 - highest resolution (benchmark case from Spiegelman et al)
+
+problem_size = 1
+
+# For testing and automatic generation of notebook output,
+# over-ride the problem size if the UW_TESTING_LEVEL is set
+
+uw_testing_level = os.environ.get("UW_TESTING_LEVEL")
+if uw_testing_level:
+    try:
+        problem_size = int(uw_testing_level)
+    except ValueError:
+        # Accept the default value
+        pass
+
+
+
+
+
+
+
# Set some things
+k = 1.0
+f = 0.0
+t_i = 2.0
+t_o = 1.0
+r_i = 0.5
+r_o = 1.0
+
+
+
+
+
+
+
# %%
+from underworld3.meshing import Annulus
+
+
+
+
+
+
+
# %%
+# first do 2D
+if problem_size <= 1:
+    cell_size = 0.05
+elif problem_size == 2:
+    cell_size = 0.02
+elif problem_size == 3:
+    cell_size = 0.01
+elif problem_size >= 4:
+    cell_size = 0.0033
+
+
+
+
+
+
+
mesh = Annulus(radiusInner=r_i, radiusOuter=r_o, cellSize=cell_size)
+
+
+
+
+
+
+
t_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=2)
+
+
+
+
+
+
+
# Create Poisson object
+poisson = Poisson(mesh, u_Field=t_soln)
+poisson.constitutive_model = uw.constitutive_models.DiffusionModel
+poisson.constitutive_model.Parameters.diffusivity = 1
+
+
+
+
+
+
+
poisson.f = f
+
+
+
+
+
+
+
poisson.petsc_options["snes_rtol"] = 1.0e-6
+poisson.petsc_options.delValue("ksp_monitor")
+poisson.petsc_options.delValue("ksp_rtol")
+
+
+
+
+
+
+
# %%
+mesh.dm.view()
+
+
+
+
+
+
+
# %%
+import sympy
+
+
+
+
+
+
+
poisson.add_dirichlet_bc(t_i, "Lower", 0)
+poisson.add_dirichlet_bc(t_o, "Upper", 0)
+
+
+
+
+
+
+
# %%
+poisson.solve()
+
+
+
+
+
+
+
# poisson.snes.view()
+
+
+
+
+
+
+
# %%
+# Check. Construct simple solution for above config.
+import math
+
+
+
+
+
+
+
A = (t_i - t_o) / (sympy.log(r_i) - math.log(r_o))
+B = t_o - A * sympy.log(r_o)
+sol = A * sympy.log(sympy.sqrt(mesh.N.x**2 + mesh.N.y**2)) + B
+
+
+
+
+
+
+
with mesh.access():
+    mesh_analytic_soln = uw.function.evaluate(sol, mesh.data, mesh.N)
+    mesh_numerical_soln = uw.function.evaluate(t_soln.fn, mesh.data, mesh.N)
+
+
+
+
+
+
+
import numpy as np
+
+
+
+
+
+
+
if not np.allclose(mesh_analytic_soln, mesh_numerical_soln, rtol=0.01):
+    raise RuntimeError("Unexpected values encountered.")
+
+
+
+
+
+
+
# %%
+poisson.constitutive_model.Parameters.diffusivity = 1.0 + 0.1 * poisson.u.fn**1.5
+poisson.f = 0.01 * poisson.u.sym[0] ** 0.5
+poisson.solve(zero_init_guess=False)
+
+
+
+
+

Validate

+
+
+
if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh)
+    pvmesh.point_data["T"] = mesh_analytic_soln
+    pvmesh.point_data["T2"] = mesh_numerical_soln
+    pvmesh.point_data["DT"] = mesh_analytic_soln - mesh_numerical_soln
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        scalars="DT",
+        use_transparency=False,
+        opacity=0.5,
+    )
+
+    pl.camera_position = "xy"
+
+    pl.show(cpos="xy")
+    # pl.screenshot(filename="test.png")
+
+
+
+
+
+
+
# %%
+
+expt_name = "Poisson-Annulus"
+outdir = "output"
+os.makedirs(f"{outdir}", exist_ok=True)
+
+
+mesh.write_timestep(
+    expt_name, meshUpdates=True, meshVars=[t_soln], outputPath=outdir, index=0
+)
+
+
+# savefile = "output/poisson_disc.h5"
+# mesh.save(savefile)
+# poisson.u.save(savefile)
+# mesh.generate_xdmf(savefile)
+
+
+
+
+
+
+
# %%
+from underworld3.meshing import SphericalShell
+from underworld3.meshing import SegmentedSphere
+
+
+
+
+
+
+
# %%
+# now do 3D
+
+problem_size = 1
+
+if problem_size <= 1:
+    cell_size = 0.3
+elif problem_size == 1:
+    cell_size = 0.15
+elif problem_size == 2:
+    cell_size = 0.05
+elif problem_size == 3:
+    cell_size = 0.02
+
+mesh_3d = SphericalShell(
+    radiusInner=r_i,
+    radiusOuter=r_o,
+    cellSize=cell_size,
+    refinement=1,
+)
+
+# mesh_3d = SegmentedSphere(radiusInner=r_i,
+#                          radiusOuter=r_o,
+#                          cellSize=cell_size
+#                         )
+
+
+t_soln_3d = uw.discretisation.MeshVariable("T", mesh_3d, 1, degree=2)
+
+
+
+
+
+
+
mesh_3d.dm.view()
+
+
+
+
+
+
+
# Create Poisson object
+poisson = Poisson(mesh_3d, u_Field=t_soln_3d)
+poisson.constitutive_model = uw.constitutive_models.DiffusionModel
+poisson.constitutive_model.Parameters.diffusivity = k
+poisson.f = f
+
+
+
+
+
+
+
poisson.petsc_options["snes_rtol"] = 1.0e-6
+poisson.petsc_options.delValue("ksp_rtol")
+
+
+
+
+
+
+
import sympy
+
+
+
+
+
+
+
poisson.add_dirichlet_bc(t_i, "Lower", 0)
+poisson.add_dirichlet_bc(t_o, "Upper", 0)
+
+
+
+
+
+
+
# Solve time
+poisson.solve()
+
+
+
+
+

Check. Construct simple solution for above config.

+
+
+
A = (t_i - t_o) / (1 / r_i - 1 / r_o)
+B = t_o - A / r_o
+sol = A / (sympy.sqrt(mesh_3d.N.x**2 + mesh_3d.N.y**2 + mesh_3d.N.z**2)) + B
+
+
+
+
+
+
+
with mesh.access():
+    mesh_analytic_soln = uw.function.evaluate(sol, mesh_3d.data, mesh_3d.N)
+    mesh_numerical_soln = uw.function.evaluate(t_soln_3d.fn, mesh_3d.data, mesh_3d.N)
+
+
+
+
+
+
+
import numpy as np
+
+
+
+
+
+
+
if not np.allclose(mesh_analytic_soln, mesh_numerical_soln, rtol=0.1):
+    raise RuntimeError("Unexpected values encountered.")
+
+
+
+
+

Validate

+
+
+
from mpi4py import MPI
+
+
+
+
+
+
+
if MPI.COMM_WORLD.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    
+    pvmesh = vis.mesh_to_pv_mesh(mesh_3d)
+    pvmesh.point_data["T"] = mesh_analytic_soln
+    pvmesh.point_data["T2"] = mesh_numerical_soln
+    pvmesh.point_data["DT"] = pvmesh.point_data["T"] - pvmesh.point_data["T2"]
+    
+    clipped = pvmesh.clip(origin=(0.001, 0.0, 0.0), normal=(1, 0, 0), invert=True)
+
+    pl = pv.Plotter()
+
+    pl.add_mesh(
+        clipped,
+        cmap="coolwarm",
+        edge_color="Grey",
+        show_edges=True,
+        scalars="T2",
+        use_transparency=False,
+        opacity=1.0,
+    )
+
+    pl.camera_position = "xy"
+
+    pl.show(cpos="xy")
+    # pl.screenshot(filename="test.png")
+
+
+
+
+
+
+
# %%
+expt_name = "Poisson-Sphere"
+outdir = "output"
+os.makedirs(f"{outdir}", exist_ok=True)
+
+mesh_3d.write_timestep(
+    expt_name, meshUpdates=True, meshVars=[t_soln_3d], outputPath=outdir, index=0
+)
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-PoissonEquation/Readme.html b/main/Notebooks/Examples-PoissonEquation/Readme.html new file mode 100644 index 0000000..50725dd --- /dev/null +++ b/main/Notebooks/Examples-PoissonEquation/Readme.html @@ -0,0 +1,572 @@ + + + + + + + + + + + Solving the Poisson Equation — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Solving the Poisson Equation

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

Solving the Poisson Equation#

+
+

Recent solver and visualization updates#

+
    +
  • [x] Ex_DiffusionSLCN_Cartesian-NL.py

  • +
  • [x] Ex_DiffusionSLCN_Cartesian.py

  • +
  • [x] Ex_Poisson_Cartesian_Generic.py

  • +
  • [x] Ex_Poisson_Cartesian.py

  • +
  • [x] Ex_Poisson_Gradient_Recovery.py

  • +
  • [x] Ex_Poisson_Spherical.py

  • +
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-PoissonEquation/output/README.html b/main/Notebooks/Examples-PoissonEquation/output/README.html new file mode 100644 index 0000000..155a79f --- /dev/null +++ b/main/Notebooks/Examples-PoissonEquation/output/README.html @@ -0,0 +1,519 @@ + + + + + + + + + + + Poisson model outputs — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Poisson model outputs

+ +
+
+ +
+
+
+ + + + +
+ +
+

Poisson model outputs#

+

These files are NOT under version control

+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-PorousFlow/Ex_Darcy_1D_benchmark-Swarm.html b/main/Notebooks/Examples-PorousFlow/Ex_Darcy_1D_benchmark-Swarm.html new file mode 100644 index 0000000..47644ae --- /dev/null +++ b/main/Notebooks/Examples-PorousFlow/Ex_Darcy_1D_benchmark-Swarm.html @@ -0,0 +1,882 @@ + + + + + + + + + + + Darcy flow (1d) using swarm variable to define permeability — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Darcy flow (1d) using swarm variable to define permeability

+ +
+
+ +
+
+
+ + + + +
+ +
+

Darcy flow (1d) using swarm variable to define permeability#

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
from petsc4py import PETSc
+import underworld3 as uw
+import numpy as np
+import sympy
+
+options = PETSc.Options()
+
+
+
+
+
+
+
minX, maxX = -1.0, 0.0
+minY, maxY = -1.0, 0.0
+
+mesh = uw.meshing.UnstructuredSimplexBox(
+    minCoords=(minX, minY), maxCoords=(maxX, maxY), cellSize=0.05, qdegree=3
+)
+
+# mesh = uw.meshing.StructuredQuadBox(elementRes=(20,20),
+#                                       minCoords=(minX,minY),
+#                                       maxCoords=(maxX,maxY),)
+
+
+p_soln = uw.discretisation.MeshVariable("P", mesh, 1, degree=3)
+v_soln = uw.discretisation.MeshVariable("U", mesh, mesh.dim, degree=2)
+
+# x and y coordinates
+x = mesh.N.x
+y = mesh.N.y
+
+
+
+
+
+
+

+if uw.mpi.size == 1:
+    # plot the mesh
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh)
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        use_transparency=False,
+    )
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
# Create Darcy Solver
+darcy = uw.systems.SteadyStateDarcy(mesh, h_Field=p_soln, v_Field=v_soln)
+darcy.petsc_options.delValue("ksp_monitor")
+darcy.petsc_options[
+    "snes_rtol"
+] = 1.0e-6  # Needs to be smaller than the contrast in properties
+darcy.constitutive_model = uw.constitutive_models.DiffusionModel
+darcy.constitutive_model.Parameters.diffusivity = 1
+
+
+
+
+
+
+
swarm = uw.swarm.Swarm(mesh=mesh)
+material = uw.swarm.IndexSwarmVariable("M", swarm, indices=2, proxy_continuous=False)
+# k = uw.swarm.IndexSwarmVariable("k", swarm, indices=2)
+
+swarm.populate(fill_param=2)
+
+
+
+
+
+
+
# Groundwater pressure boundary condition on the bottom wall
+
+max_pressure = 0.5
+initialPressure = -1.0 * y * max_pressure
+
+
+
+
+
+
+
# set up two materials
+interfaceY = -0.26
+
+
+from sympy import Piecewise, ceiling, Abs
+
+k1 = 1.0
+k2 = 1.0e-4
+
+# # The piecewise version
+# kFunc = Piecewise((k1, y >= interfaceY), (k2, y < interfaceY), (1.0, True))
+
+# darcy.constitutive_model.material_properties = darcy.constitutive_model.Parameters(diffusivity=kFunc)
+
+
+
+
+
+
+
with swarm.access(material):
+    material.data[swarm.data[:, 1] >= interfaceY] = 0
+    material.data[swarm.data[:, 1] < interfaceY] = 1
+
+
+
+
+
+
+
mat_k = np.array([k1, k2])
+
+kFn = mat_k[0] * material.sym[0] + mat_k[1] * material.sym[1]
+
+darcy.constitutive_model.Parameters.diffusivity = kFn
+
+
+
+
+
+
+
# A smooth version
+# kFunc = k2 + (k1-k2) * (0.5 + 0.5 * sympy.tanh(100.0*(y-interfaceY)))
+
+darcy.f = 0.0
+darcy.s = sympy.Matrix([0, -1]).T
+
+# set up boundary conditions
+darcy.add_dirichlet_bc(0.0, "Top")
+darcy.add_dirichlet_bc(-1.0 * minY * max_pressure, "Bottom")
+
+# Zero pressure gradient at sides / base (implied bc)
+
+darcy._v_projector.petsc_options["snes_rtol"] = 1.0e-6
+darcy._v_projector.smoothing = 1.0e-6
+darcy._v_projector.add_dirichlet_bc(0.0, "Left", 0)
+darcy._v_projector.add_dirichlet_bc(0.0, "Right", 0)
+
+
+
+
+
+
+
# Solve time
+darcy.solve()
+
+
+
+
+
+
+
if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh)
+    pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym)
+    pvmesh.point_data["K"] = vis.scalar_fn_to_pv_points(pvmesh, kFn)
+    pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)
+
+    velocity_points = vis.meshVariable_to_pv_cloud(v_soln)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym)
+
+    # point sources at cell centres
+    points = np.zeros((mesh._centroids.shape[0], 3))
+    points[:, 0] = mesh._centroids[:, 0]
+    points[:, 1] = mesh._centroids[:, 1]
+    point_cloud0 = pv.PolyData(points[::3])
+
+
+    pvstream = pvmesh.streamlines_from_source(
+                                                point_cloud0,
+                                                vectors="V",
+                                                integrator_type=45,
+                                                integration_direction="both",
+                                                max_steps=1000,
+                                                max_time=0.25,
+                                                initial_step_length=0.001,
+                                                max_step_length=0.01,
+                                            )
+
+    points = vis.swarm_to_pv_cloud(swarm)
+    point_cloud1 = pv.PolyData(points)
+    point_cloud1.point_data["K"] = vis.scalar_fn_to_pv_points(point_cloud1, kFn)
+
+    with swarm.access():
+        point_cloud1.point_data["M"] = material.data.copy()
+    
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        scalars="P",
+        use_transparency=False,
+        opacity=1.0,
+    )
+
+    pl.add_mesh(
+        point_cloud1,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=False,
+        scalars="M",
+        use_transparency=False,
+        opacity=0.95,
+    )
+
+    pl.add_mesh(pvstream, line_width=10.0)
+
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.005, opacity=0.75)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
# set up interpolation coordinates
+ycoords = np.linspace(minY + 0.001 * (maxY - minY), maxY - 0.001 * (maxY - minY), 100)
+xcoords = np.full_like(ycoords, -1)
+xy_coords = np.column_stack([xcoords, ycoords])
+
+pressure_interp = uw.function.evaluate(p_soln.sym[0], xy_coords)
+
+
+
+
+
+
+
La = -1.0 * interfaceY
+Lb = 1.0 + interfaceY
+dP = max_pressure
+
+S = 1
+Pa = (dP / Lb - S + k1 / k2 * S) / (1.0 / Lb + k1 / k2 / La)
+pressure_analytic = np.piecewise(
+    ycoords,
+    [ycoords >= -La, ycoords < -La],
+    [
+        lambda ycoords: -Pa * ycoords / La,
+        lambda ycoords: Pa + (dP - Pa) * (-ycoords - La) / Lb,
+    ],
+)
+
+S = 0
+Pa = (dP / Lb - S + k1 / k2 * S) / (1.0 / Lb + k1 / k2 / La)
+pressure_analytic_noG = np.piecewise(
+    ycoords,
+    [ycoords >= -La, ycoords < -La],
+    [
+        lambda ycoords: -Pa * ycoords / La,
+        lambda ycoords: Pa + (dP - Pa) * (-ycoords - La) / Lb,
+    ],
+)
+
+
+
+
+
+
+
import matplotlib.pyplot as plt
+
+%matplotlib inline
+
+fig = plt.figure()
+ax1 = fig.add_subplot(111, xlabel="Pressure", ylabel="Depth")
+ax1.plot(pressure_interp, ycoords, linewidth=3, label="Numerical solution")
+ax1.plot(
+    pressure_analytic, ycoords, linewidth=3, linestyle="--", label="Analytic solution"
+)
+ax1.plot(
+    pressure_analytic_noG,
+    ycoords,
+    linewidth=3,
+    linestyle="--",
+    label="Analytic (no gravity)",
+)
+ax1.grid("on")
+ax1.legend()
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-PorousFlow/Ex_Darcy_1D_benchmark.html b/main/Notebooks/Examples-PorousFlow/Ex_Darcy_1D_benchmark.html new file mode 100644 index 0000000..5ccd654 --- /dev/null +++ b/main/Notebooks/Examples-PorousFlow/Ex_Darcy_1D_benchmark.html @@ -0,0 +1,825 @@ + + + + + + + + + + + Darcy flow (1d) using xy coordinates to define permeability distribution — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Darcy flow (1d) using xy coordinates to define permeability distribution

+ +
+
+ +
+
+
+ + + + +
+ +
+

Darcy flow (1d) using xy coordinates to define permeability distribution#

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
from petsc4py import PETSc
+import underworld3 as uw
+import numpy as np
+import sympy
+
+options = PETSc.Options()
+
+
+
+
+
+
+
minX, maxX = -1.0, 0.0
+minY, maxY = -1.0, 0.0
+
+mesh = uw.meshing.UnstructuredSimplexBox(
+    minCoords=(minX, minY), maxCoords=(maxX, maxY), cellSize=0.05, qdegree=3
+)
+
+
+# x and y coordinates
+x = mesh.N.x
+y = mesh.N.y
+
+
+
+
+
+
+
# Create Darcy Solver
+darcy = uw.systems.SteadyStateDarcy(mesh)
+
+p_soln = darcy.Unknowns.u
+v_soln = darcy.v
+
+darcy.petsc_options[
+    "snes_rtol"
+] = 1.0e-6  # Needs to be smaller than the contrast in properties
+
+darcy.constitutive_model = uw.constitutive_models.DarcyFlowModel
+darcy.constitutive_model.Parameters.permeability = 1
+
+
+
+
+
+
+
p_soln_0 = p_soln.clone("P_no_g", r"{p_\textrm{no g}}")
+v_soln_0 = v_soln.clone("V_no_g", r"{v_\textrm{no g}}")
+
+
+
+
+
+
+
# Groundwater pressure boundary condition on the bottom wall
+
+max_pressure = 0.5
+
+
+
+
+
+
+
# set up two materials
+
+interfaceY = -0.26
+
+from sympy import Piecewise, ceiling, Abs
+
+k1 = 1.0
+k2 = 1.0e-4
+
+# The piecewise version
+kFunc = Piecewise((k1, y >= interfaceY), (k2, y < interfaceY), (1.0, True))
+
+# A smooth version
+
+darcy.constitutive_model.Parameters.permeability = kFunc
+darcy.constitutive_model.Parameters.s = sympy.Matrix([0, 0]).T
+darcy.f = 0.0
+
+# set up boundary conditions
+darcy.add_dirichlet_bc(0.0, "Top")
+darcy.add_dirichlet_bc(-1.0 * minY * max_pressure, "Bottom")
+
+
+
+
+
+
+
# Solve time
+darcy.solve()
+with mesh.access(p_soln_0, v_soln_0):
+    p_soln_0.data[...] = p_soln.data[...]
+    v_soln_0.data[...] = v_soln.data[...]
+
+
+
+
+
+
+
# now switch on gravity
+
+darcy.constitutive_model.Parameters.s = sympy.Matrix([0, -1]).T
+darcy.solve()
+
+
+
+
+
+
+
if uw.mpi.size == 1:
+
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh)
+    pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym)
+    pvmesh.point_data["K"] = vis.scalar_fn_to_pv_points(pvmesh, kFunc)
+    pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)
+
+    velocity_points = vis.meshVariable_to_pv_cloud(v_soln)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym)
+
+    # point sources at cell centres
+    points = np.zeros((mesh._centroids.shape[0], 3))
+    points[:, 0] = mesh._centroids[:, 0]
+    points[:, 1] = mesh._centroids[:, 1]
+    point_cloud = pv.PolyData(points[::3])
+
+    pvstream = pvmesh.streamlines_from_source(
+                                                point_cloud,
+                                                vectors="V",
+                                                integrator_type=45,
+                                                integration_direction="both",
+                                                max_steps=1000,
+                                                max_time=0.1,
+                                                initial_step_length=0.001,
+                                                max_step_length=0.01,
+                                            )
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        scalars="P",
+        use_transparency=False,
+        opacity=1.0,
+    )
+
+    pl.add_mesh(pvstream, line_width=1.0)
+
+    # pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.005, opacity=0.75)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
# set up interpolation coordinates
+ycoords = np.linspace(minY + 0.001 * (maxY - minY), maxY - 0.001 * (maxY - minY), 100)
+xcoords = np.full_like(ycoords, -1)
+xy_coords = np.column_stack([xcoords, ycoords])
+
+pressure_interp = uw.function.evalf(p_soln.sym[0], xy_coords)
+pressure_interp_0 = uw.function.evalf(p_soln_0.sym[0], xy_coords)
+
+
+
+
+
+
+
La = -1.0 * interfaceY
+Lb = 1.0 + interfaceY
+dP = max_pressure
+
+S = 1
+Pa = (dP / Lb - S + k1 / k2 * S) / (1.0 / Lb + k1 / k2 / La)
+pressure_analytic = np.piecewise(
+    ycoords,
+    [ycoords >= -La, ycoords < -La],
+    [
+        lambda ycoords: -Pa * ycoords / La,
+        lambda ycoords: Pa + (dP - Pa) * (-ycoords - La) / Lb,
+    ],
+)
+
+S = 0
+Pa = (dP / Lb - S + k1 / k2 * S) / (1.0 / Lb + k1 / k2 / La)
+pressure_analytic_noG = np.piecewise(
+    ycoords,
+    [ycoords >= -La, ycoords < -La],
+    [
+        lambda ycoords: -Pa * ycoords / La,
+        lambda ycoords: Pa + (dP - Pa) * (-ycoords - La) / Lb,
+    ],
+)
+
+
+
+
+
+
+
import matplotlib.pyplot as plt
+
+%matplotlib inline
+
+fig = plt.figure()
+ax1 = fig.add_subplot(111, xlabel="Pressure", ylabel="Depth")
+ax1.plot(pressure_interp, ycoords, linewidth=3, label="Numerical solution")
+ax1.plot(pressure_interp_0, ycoords, linewidth=3, label="Numerical solution (no G)")
+ax1.plot(
+    pressure_analytic, ycoords, linewidth=3, linestyle="--", label="Analytic solution"
+)
+ax1.plot(
+    pressure_analytic_noG,
+    ycoords,
+    linewidth=3,
+    linestyle="--",
+    label="Analytic (no gravity)",
+)
+ax1.grid("on")
+ax1.legend()
+
+
+
+
+
+
+
darcy.view()
+
+
+
+
+
+
+
darcy.darcy_flux
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-PorousFlow/Ex_Darcy_Cartesian.html b/main/Notebooks/Examples-PorousFlow/Ex_Darcy_Cartesian.html new file mode 100644 index 0000000..a45b1c8 --- /dev/null +++ b/main/Notebooks/Examples-PorousFlow/Ex_Darcy_Cartesian.html @@ -0,0 +1,830 @@ + + + + + + + + + + + Underworld Groundwater Flow Benchmark 1 — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Underworld Groundwater Flow Benchmark 1

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

Underworld Groundwater Flow Benchmark 1#

+

See the Underworld2 example by Adam Beall.

+

Flow driven by gravity and topography. We check the flow for constant permeability and for exponentially decreasing permeability as a function of depth.

+

Note, this benchmark is a bit problematic because the surface shape is not really +consistent with the sidewall boundary conditions - zero gradients at the vertical boundaries.If we replace the sin(x) term with cos(x) to describe the surface then it works a little better because there is no kink in the surface topography at the walls.

+

Note, there is not an obvious way in pyvista to make the streamlines smaller / shorter / fainter where flow rates are very low so the visualisation is a little misleading right now.

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
# %%
+from petsc4py import PETSc
+import underworld3 as uw
+import numpy as np
+import sympy
+
+
+
+
+
+
+
options = PETSc.Options()
+
+
+
+
+
+
+
# %%
+mesh = uw.meshing.UnstructuredSimplexBox(
+    minCoords=(0.0, 0.0), maxCoords=(4.0, 1.0), cellSize=0.05, qdegree=3
+)
+
+
+
+
+
+
+
p_soln = uw.discretisation.MeshVariable("P", mesh, 1, degree=2)
+v_soln = uw.discretisation.MeshVariable("U", mesh, mesh.dim, degree=1, continuous=False)
+
+
+
+
+

Mesh deformation

+
+
+
x, y = mesh.X
+
+
+
+
+
+
+
h_fn = 1.0 + x * 0.2 / 4 + 0.04 * sympy.cos(2.0 * np.pi * x) * y
+
+
+
+
+
+
+
new_coords = mesh.data.copy()
+new_coords[:, 1] = uw.function.evaluate(h_fn * y, mesh.data, mesh.N)
+
+
+
+
+
+
+
mesh.deform_mesh(new_coords=new_coords)
+
+
+
+
+
+
+
# %%
+if uw.mpi.size == 1 and uw.is_notebook:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh)
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        use_transparency=False,
+    )
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
# %%
+# Create Poisson object
+darcy = uw.systems.SteadyStateDarcy(mesh, h_Field=p_soln, v_Field=v_soln)
+darcy.constitutive_model = uw.constitutive_models.DarcyFlowModel
+darcy.constitutive_model.Parameters.permeability = 1
+darcy.petsc_options.delValue("ksp_monitor")
+
+
+
+
+

Set some things

+
+
+
k = sympy.exp(-2.0 * 2.302585 * (h_fn - y))  # powers of 10
+darcy.constitutive_model.Parameters.permeability = k
+
+
+
+
+
+
+
k
+
+
+
+
+
+
+
darcy.f = 0.0
+darcy.constitutive_model.Parameters.s = sympy.Matrix([0, -1]).T
+
+
+
+
+
+
+
darcy.add_dirichlet_bc(0.0, "Top")
+
+
+
+
+

Zero pressure gradient at sides / base (implied bc)

+
+
+
darcy._v_projector.smoothing = 0.0
+
+
+
+
+
+
+
# %%
+# Solve time
+darcy.petsc_options.setValue("snes_monitor", None)
+darcy.solve(verbose=False)
+
+
+
+
+
+
+
# %%
+if uw.mpi.size == 1 and uw.is_notebook:
+
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh)
+    pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)
+
+    pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym)
+    pvmesh.point_data["dP"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym[0] - (h_fn - y))
+    pvmesh.point_data["K"] = vis.scalar_fn_to_pv_points(pvmesh, k)
+    pvmesh.point_data["S"] = vis.scalar_fn_to_pv_points(pvmesh, sympy.log(v_soln.sym.dot(v_soln.sym)))
+
+    velocity_points = vis.meshVariable_to_pv_cloud(v_soln)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym)
+
+    # point sources at cell centres
+
+    points = np.zeros((mesh._centroids.shape[0], 3))
+    points[:, 0] = mesh._centroids[:, 0]
+    points[:, 1] = mesh._centroids[:, 1]
+    point_cloud = pv.PolyData(points[::3])
+
+    pvstream = pvmesh.streamlines_from_source(
+                                                point_cloud,
+                                                vectors="V",
+                                                integrator_type=45,
+                                                integration_direction="both",
+                                                max_steps=1000,
+                                                max_time=0.2,
+                                                initial_step_length=0.001,
+                                                max_step_length=0.01,
+                                            )
+
+    pl = pv.Plotter()
+
+    pl.add_mesh(
+                pvmesh,
+                cmap="coolwarm",
+                edge_color="Black",
+                show_edges=True,
+                scalars="P",
+                use_transparency=False,
+                opacity=1.0,
+            )
+
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.5, opacity=0.5)
+    pl.add_mesh(pvstream, line_width=1.0)
+    pl.show(cpos="xy")
+
+
+
+
+
+

Metrics#

+
+
+
_, _, _, max_p, _, _, _ = p_soln.stats()
+
+
+
+
+
+
+

+print("Max pressure         :   {:4f}".format(max_p))
+
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-PorousFlow/Ex_viscousFingering-Viz.html b/main/Notebooks/Examples-PorousFlow/Ex_viscousFingering-Viz.html new file mode 100644 index 0000000..d8799b8 --- /dev/null +++ b/main/Notebooks/Examples-PorousFlow/Ex_viscousFingering-Viz.html @@ -0,0 +1,797 @@ + + + + + + + + + + + Viscous fingering model — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + +
+ +
+

Viscous fingering model#

+

Based on Darcy flow and advection-diffusion of two fluids with varying viscosity.

+

From Guy Simpson - Practical Finite Element Modeling in Earth Science using Matlab (2017)

+
    +
  • Section 10.2 of the book

  • +
+
+

Darcy pressure solution (quasi-static)#

+
+\[\nabla \cdot \left( \boldsymbol\kappa \nabla p - \boldsymbol{s} \right) + W = 0\]
+
+
+

Darcy velocity:#

+
+\[u = - \frac{k}{\mu_c}\nabla p\]
+
+\[\nabla \cdot \mathbf{u} = 0\]
+
+
+

viscosity:#

+
+\[\mu_c = \left( \frac{c}{\mu_o^{\frac{1}{4}}} + \frac{1-c}{\mu_s^{\frac{1}{4}}} \right)^{-4}\]
+
+

Advection-diffusion of material type (solvent / oil):#

+
+\[\varphi \frac{\partial c}{\partial t} + \mathbf{u} \cdot \nabla c = \nabla(\kappa\nabla c)\]
+
+

Model physical parameters:#

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

parameter

symbol

value

units

x

$\(10\)$

$\(m\)$

y

$\(10\)$

$\(m\)$

permeability

$\(k\)$

$\(10^{-13}\)$

$\(m^2\)$

porosity

$\(\varphi\)$

$\(0.1\)$

diffusivity

$\(\kappa\)$

$\(10^{-9}\)$

$\(m^2 s^{-1}\)$

viscosity (solvent)

$\(\eta{_s}\)$

$\(1.33{\cdot}10^{-4}\)$

$\(Pa s\)$

viscosity (oil)

$\(\eta{_o}\)$

$\(20\eta_s\)$

$\(Pa s\)$

pressure

$\(p\)$

$\(10^{5}\)$

$\(Pa\)$

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
from petsc4py import PETSc
+import underworld3 as uw
+import numpy as np
+import sympy
+
+from scipy.interpolate import griddata, interp1d
+
+import matplotlib.pyplot as plt
+
+import os
+
+
+
+
+
+
+
%%sh
+
+ls -trl /Users/lmoresi/+Simulations/PorousFlow/viscousFingering_example_8/* | tail -10
+
+
+
+
+
+
+
## Reading the checkpoints back in ... 
+
+step = 95
+
+checkpoint_dir = "/Users/lmoresi/+Simulations/PorousFlow/viscousFingering_example_7"
+checkpoint_base = "simpson_ViscousFinger"
+base_filename = os.path.join(checkpoint_dir, checkpoint_base)
+
+
+
+
+
+
+
mesh = uw.discretisation.Mesh(f"{base_filename}.mesh.00000.h5")
+
+x,y = mesh.X
+
+minX = mesh.data[:,0].min()
+minY = mesh.data[:,1].min()
+maxX = mesh.data[:,0].max()
+maxY = mesh.data[:,1].max()
+
+v_soln_ckpt = uw.discretisation.MeshVariable("U", mesh, mesh.dim, degree=1)
+p_soln_ckpt = uw.discretisation.MeshVariable("P", mesh, 1, degree=2)
+mat_ckpt = uw.discretisation.MeshVariable("omega", mesh, 1, degree=3)
+
+vizmesh = uw.meshing.UnstructuredSimplexBox(
+    minCoords=(minX, minY), maxCoords=(maxX, maxY), cellSize=maxY/300, qdegree=1)
+
+
+v_soln_ckpt.read_timestep(checkpoint_base, "U", step, outputPath=checkpoint_dir)
+p_soln_ckpt.read_timestep(checkpoint_base, "P", step, outputPath=checkpoint_dir)
+mat_ckpt.read_timestep(checkpoint_base, "mat", step, outputPath=checkpoint_dir)
+
+
+
+
+
+
+
if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(vizmesh)
+    pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln_ckpt.sym)
+    pvmesh.point_data["mat"] = vis.scalar_fn_to_pv_points(pvmesh, mat_ckpt.sym)
+    pvmesh.point_data["p"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln_ckpt.sym)
+    
+    velocity_points = vis.meshVariable_to_pv_cloud(v_soln_ckpt)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln_ckpt.sym)/vis.vector_fn_to_pv_points(velocity_points, v_soln_ckpt.sym).max()
+    
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_mesh(pvmesh, style="wireframe", cmap="RdYlBu_r", edge_color="Grey", scalars="mat",
+                show_edges=True, line_width=0.05, use_transparency=False, opacity=1)
+    
+#     pl.add_mesh(pvmesh, cmap="RdYlBu_r", edge_color="Grey", scalars="mat",
+#                 show_edges=False, use_transparency=False, opacity=0.5)
+  
+
+
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=250, opacity=1)
+
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
I = uw.maths.Integral(mesh, sympy.sqrt(v_soln_ckpt.sym.dot(v_soln_ckpt.sym)))
+Vrms = I.evaluate() 
+I.fn = 1.0
+Vrms /= I.evaluate()
+Vrms
+
+
+
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-PorousFlow/Ex_viscousFingering.html b/main/Notebooks/Examples-PorousFlow/Ex_viscousFingering.html new file mode 100644 index 0000000..8740349 --- /dev/null +++ b/main/Notebooks/Examples-PorousFlow/Ex_viscousFingering.html @@ -0,0 +1,1162 @@ + + + + + + + + + + + Viscous fingering model — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + +
+ +
+

Viscous fingering model#

+

Based on Darcy flow and advection-diffusion of two fluids with varying viscosity. +From Simpson, 2017, and from Homsy, 1987, fingering patterns develop under certain conditions.

+
+

Darcy model (quasi-static)#

+

The Darcy equation for the steady-state pressure field can be written

+
+\[\nabla \cdot \left( \boldsymbol\kappa \nabla p - \boldsymbol{s} \right) = 0\]
+
+

Darcy velocity:#

+

The model from Homsy (1987) generally assumes \(\nabla \cdot \mathbf{u} = 0\) which is equivalent to the Darcy flow equation with \(\boldsymbol{s}=0\) +and using

+
+\[\mathbf{u} = - \frac{k}{\mu_c}\nabla p\]
+
+
+

viscosity:#

+
+\[\mu_c = \left( \frac{c}{\mu_o^{{1}/{4}}} + \frac{1-c}{\mu_s^{{1}/{4}}} \right)^{-4}\]
+
+
+

Advection-diffusion of material:#

+
+\[\varphi \frac{\partial c}{\partial t} + \varphi (\mathbf{u} \cdot \nabla) c= \nabla(\kappa\nabla c)\]
+

If the diffusion coefficient is small, then it is often more appropriate to assume pure transport

+
+\[\varphi \frac{D c}{D t} = \nabla(\kappa\nabla c) \approx 0\]
+
+

Model physical parameters:#

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

parameter

symbol

value

units

x

$\(10\)$

$\(m\)$

y

$\(10\)$

$\(m\)$

permeability

$\(k\)$

$\(10^{-13}\)$

$\(m^2\)$

porosity

$\(\varphi\)$

$\(0.1\)$

diffusivity

$\(\kappa\)$

$\(10^{-9}\)$

$\(m^2 s^{-1}\)$

viscosity (solvent)

$\(\mu{_s}\)$

$\(1.33{\cdot}10^{-4}\)$

$\(Pa s\)$

viscosity (oil)

$\(\mu{_o}\)$

$\(20\eta_s\)$

$\(Pa s\)$

pressure

$\(p\)$

$\(10^{5}\)$

$\(Pa\)$

+
+
+
+
+
+

References#

+

Homsy, G. M. (1987). Viscous Fingering in Porous Media. Annual Review of Fluid Mechanics, 19(1), 271–311. https://doi.org/10.1146/annurev.fl.19.010187.001415

+

Guy Simpson - Practical Finite Element Modeling in Earth Science using Matlab (2017)

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
from petsc4py import PETSc
+import underworld3 as uw
+import numpy as np
+import sympy
+
+# from scipy.interpolate import griddata, interp1d
+
+import matplotlib.pyplot as plt
+
+import os
+
+options = PETSc.Options()
+
+
+
+
+
+
+
outputDir = "./output/viscousFingering_example/"
+
+if uw.mpi.rank == 0:
+    ### create folder if required
+    os.makedirs(outputDir, exist_ok=True)
+
+
+
+
+
+
+
# import unit registry to make it easy to convert between units
+u = uw.scaling.units
+
+### make scaling easier
+ndim, nd = uw.scaling.non_dimensionalise, uw.scaling.non_dimensionalise
+dim = uw.scaling.dimensionalise
+
+refLength = 10  ### length and height of box in meters
+g = 9.81
+eta = 1.33e-4
+kappa = 1e-9  ### m^2/s
+perm = 1e-13  ### m^2
+porosity = 0.1
+T_0 = 273.15
+T_1 = 1573.15
+dT = T_1 - T_0
+rho0 = 1e3
+
+
+refTime = perm / kappa
+refViscosity = eta * u.pascal * u.second
+
+KL = refLength * u.meter
+KL = 1.0 * u.millimetre
+KT = dT * u.kelvin
+Kt = refTime * u.seconds
+Kt = 0.01 * u.year
+KM = refViscosity * KL * Kt
+
+### create unit registry
+scaling_coefficients = uw.scaling.get_coefficients()
+scaling_coefficients["[length]"] = KL
+scaling_coefficients["[time]"] = Kt
+scaling_coefficients["[mass]"] = KM
+scaling_coefficients["[temperature]"] = KT
+scaling_coefficients
+
+
+
+
+
+
+
minX, maxX = 0, nd(10 * u.meter)
+minY, maxY = 0, nd(10 * u.meter)
+
+elements = 25
+
+mesh = uw.meshing.UnstructuredSimplexBox(
+    minCoords=(minX, minY), maxCoords=(maxX, maxY), cellSize=maxY / elements, qdegree=5
+)
+
+vizmesh = uw.meshing.UnstructuredSimplexBox(
+    minCoords=(minX, minY), maxCoords=(maxX, maxY), cellSize=0.5 * maxY / elements, qdegree=1
+)
+
+p_soln = uw.discretisation.MeshVariable("P", mesh, 1, degree=2)
+v_soln = uw.discretisation.MeshVariable("U", mesh, mesh.dim, degree=1)
+mat = uw.discretisation.MeshVariable("mat", mesh, 1, degree=3, continuous=True)
+
+# x and y coordinates
+x = mesh.N.x
+y = mesh.N.y
+
+
+
+
+
+
+

+if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh)
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        use_transparency=False,
+    )
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
# Create Darcy Solver
+
+darcy = uw.systems.SteadyStateDarcy(mesh, h_Field=p_soln, v_Field=v_soln)
+darcy.petsc_options.delValue("ksp_monitor")
+darcy.petsc_options[
+    "snes_rtol"
+] = 1.0e-6  # Needs to be smaller than the contrast in properties
+darcy.constitutive_model = uw.constitutive_models.DiffusionModel
+
+
+
+
+
+
+
darcy
+
+
+
+
+
+\[ +\color{Green}{\mathbf{f}_{0}} - +\nabla \cdot + \color{Blue}{{\mathbf{f}_{1}}} = + \color{Maroon}{\underbrace{\Bigl[ W \Bigl] }_{\mathbf{f}_{s}}} +\]
+
+
+
swarm = uw.swarm.Swarm(mesh=mesh, recycle_rate=5)
+
+material = swarm.add_variable(name="M", size=1, proxy_degree=mat.degree)
+conc = swarm.add_variable(name="C", size=1, proxy_degree=mat.degree)
+
+swarm.populate(fill_param=4)
+
+
+
+
+
+
+
adv_diff = uw.systems.AdvDiffusionSLCN(
+    mesh=mesh, u_Field=mat, V_fn=v_soln, #DuDt=conc.sym[0]
+)
+
+adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel
+
+
+
+
+
+

Random material distribution along the interface#

+
+
+
np.random.seed(100)
+
+### on the mesh
+
+with mesh.access(mat):
+    x0 = nd(2.5 * u.meter)
+    dx = max(mesh.get_min_radius(), nd(0.1 * u.meter))
+
+    fluctuation = nd(0.01 * u.meter) * np.cos(
+        mat.coords[:, 1] / nd(0.5 * u.meter) * np.pi
+    )
+    fluctuation += nd(0.01 * u.meter) * np.cos(
+        mat.coords[:, 1] / nd(2.0 * u.meter) * np.pi
+    )
+    fluctuation += nd(0.05 * u.meter) * np.random.random(size=mat.coords.shape[0])
+
+    mat.data[...] = 0
+    mat.data[mat.coords[:, 0] + fluctuation < x0] = 1
+
+# ### on the swarm
+
+with swarm.access(material):
+    # material.data[:,0] = mat.rbf_interpolate(new_coords=material.swarm.data, nnn=1)[:,0]
+    material.data[:, 0] = uw.function.evalf(mat.sym, swarm.particle_coordinates.data)
+
+
+
+
+
+
+
if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+    
+    pvmesh = vis.mesh_to_pv_mesh(mesh)
+    pvmesh.point_data["mat"] = vis.scalar_fn_to_pv_points(pvmesh, mat.sym)
+
+    
+    points = vis.swarm_to_pv_cloud(swarm)
+    point_cloud = pv.PolyData(points)
+
+    with swarm.access(material):
+        point_cloud.point_data["M"] = material.data.copy()
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    # pl.add_mesh(pvmesh, cmap="coolwarm", edge_color="Black", show_edges=True, use_transparency=False)
+
+    pl.add_points(
+        point_cloud,
+        cmap="coolwarm",
+        render_points_as_spheres=True,
+        point_size=10,
+        opacity=0.33,
+    )
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
eta_s = nd(1.33e-4 * u.pascal * u.second)
+eta_o = 20 * eta_s
+
+
+
+
+
+
+
### use the mesh var to map composition to viscosity
+## eta_fn = (mat.sym[0]/eta_s**0.25+(1-mat.sym[0])/eta_o**0.25)**(-4)
+
+### use the swarm var to map composition to viscosity
+eta_fn = (material.sym[0] / eta_s**0.25 + (1 - material.sym[0]) / eta_o**0.25) ** (
+    -4
+)
+
+
+
+
+
+
+
nd_perm = nd(perm * u.meter**2)
+
+diffusivity_fn = nd_perm / eta_fn
+
+darcy.constitutive_model.Parameters.diffusivity = diffusivity_fn
+
+
+
+
+
+

Darcy velocity:#

+
+\[ u = - \frac{k}{\mu_c}\nabla p\]
+
+
+
adv_diff.constitutive_model.Parameters.diffusivity = nd(1e-9 * u.meter**2 / u.second)
+
+
+
+
+
+
+
p0_nd = nd(0.1e6 * u.pascal)
+# p_dx = p0_nd * (1 - mesh.X[0])
+
+# with mesh.access(p_soln):
+#     p_soln.data[:,0] = uw.function.evaluate(p_dx, p_soln.coords, mesh.N)
+
+
+
+
+
+
+
## Make sure additional terms are set to zero
+darcy.f = 0.0
+darcy.s = sympy.Matrix([0, 0]).T
+
+### set up boundary conditions for the Darcy solver
+darcy.add_dirichlet_bc(p0_nd, "Left")
+darcy.add_dirichlet_bc(0.0, "Right")
+
+### set up boundary conditions for the adv diffusion solver
+adv_diff.add_dirichlet_bc(1.0, "Left")
+adv_diff.add_dirichlet_bc(0.0, "Right")
+
+# Zero pressure gradient at sides / base (implied bc)
+
+# darcy._v_projector.petsc_options["snes_rtol"] = 1.0e-6
+# darcy._v_projector.smoothing = 1e24
+# darcy._v_projector.add_dirichlet_bc(0.0, "Left", 0)
+# darcy._v_projector.add_dirichlet_bc(0.0, "Right", 0)
+
+
+
+
+
+
+
darcy.solve()
+
+
+
+
+
+
+
time = 0
+step = 0
+
+
+
+
+
+
+
finish_time = 0.01 * u.year
+
+# while time < nd(finish_time):
+
+for iteration in range(0, 20):
+    if uw.mpi.rank == 0:
+        print(f"\n\nstep: {step}, time: {dim(time, u.year)}")
+
+    if step % 5 == 0:
+        mesh.write_timestep(
+            "viscousFinger",
+            meshUpdates=False,
+            meshVars=[p_soln, v_soln, mat],
+            outputPath=outputDir,
+            index=step,
+        )
+
+    ### get the Darcy velocity from the darcy solve
+    darcy.solve(zero_init_guess=True)
+
+    dt = ndim(0.0002 * u.year)
+
+    ### do the advection-diffusion
+    # adv_diff.solve(timestep=dt)
+
+    ### update swarm / swarm variables
+    # with swarm.access(material):
+    # material.data[:,0] = mat.rbf_interpolate(new_coords=material.swarm.data, nnn=1)[:,0]
+    # material.data[:,0] = uw.function.evaluate(mat.sym, swarm.particle_coordinates.data)
+
+    ### advect the swarm
+    swarm.advection(
+        V_fn=v_soln.sym
+        * sympy.Matrix.diag(1 / porosity, 1 / sympy.sympify(1000000000)),
+        delta_t=dt,
+        order=2,
+        evalf=True,
+    )
+
+    # with mesh.access(v_soln):
+    #     ## Divide by the porosity to get the actual velocity
+    #     v_soln.data[:,] *= porosity
+
+    I = uw.maths.Integral(mesh, sympy.sqrt(v_soln.sym.dot(v_soln.sym)))
+    Vrms = I.evaluate()
+    I.fn = 1.0
+    Vrms /= I.evaluate()
+
+    if uw.mpi.rank == 0:
+        print(f"V_rms = {Vrms} ... delta t = {dt}.  dL = {Vrms * dt}")
+
+    step += 1
+    time += dt
+
+    if time > nd(finish_time):
+        break
+
+
+
+
+
+
+
if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+
+    pvmesh = vis.mesh_to_pv_mesh(vizmesh)
+    pvmesh.point_data["mat"] = vis.scalar_fn_to_pv_points(pvmesh, material.sym)
+    pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym)
+    
+    velocity_points = vis.meshVariable_to_pv_cloud(v_soln)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym)/vis.vector_fn_to_pv_points(velocity_points, v_soln.sym).max()
+    
+    points = vis.swarm_to_pv_cloud(swarm)
+    point_cloud = pv.PolyData(points)
+
+    with swarm.access(material):
+        point_cloud.point_data["M"] = material.data.copy()
+
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_mesh(pvmesh, style="surface", cmap="coolwarm", edge_color="Grey", scalars="P",
+                show_edges=False, use_transparency=False, opacity=1)
+
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=1250, opacity=1)
+
+    pl.add_points(
+        point_cloud,
+        cmap="coolwarm",
+        render_points_as_spheres=False,
+        point_size=2,
+        opacity=0.66,
+    )
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
mat.stats()
+
+
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-PorousFlow/Readme.html b/main/Notebooks/Examples-PorousFlow/Readme.html new file mode 100644 index 0000000..4c2074b --- /dev/null +++ b/main/Notebooks/Examples-PorousFlow/Readme.html @@ -0,0 +1,572 @@ + + + + + + + + + + + Porous flow examples — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Porous flow examples

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

Porous flow examples#

+

e.g. groundwater equations / Darcy flow

+
+

Recent solver and visualization updates#

+
    +
  • [ ] Ex_Darcy_Cartesian.py

  • +
  • [ ] Ex_Darcy_1D_benchmark-Swarm.py

  • +
  • [ ] Ex_Darcy_1D_benchmark.py

  • +
  • [ ] Ex_viscousFingering.py

  • +
  • [ ] Ex_viscousFingering-Viz.py

  • +
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-PorousFlow/output/README.html b/main/Notebooks/Examples-PorousFlow/output/README.html new file mode 100644 index 0000000..c46bf9b --- /dev/null +++ b/main/Notebooks/Examples-PorousFlow/output/README.html @@ -0,0 +1,519 @@ + + + + + + + + + + + Porous flow model outputs — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Porous flow model outputs

+ +
+
+ +
+
+
+ + + + +
+ +
+

Porous flow model outputs#

+

These files are NOT under version control

+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-PostProcessing/Ex_ChannelFlow_Visualise.html b/main/Notebooks/Examples-PostProcessing/Ex_ChannelFlow_Visualise.html new file mode 100644 index 0000000..a3d3bda --- /dev/null +++ b/main/Notebooks/Examples-PostProcessing/Ex_ChannelFlow_Visualise.html @@ -0,0 +1,719 @@ + + + + + + + + + + + <no title> — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+
+
# Visualise Channel Flow model 
+
+
+import nest_asyncio
+nest_asyncio.apply()
+import os
+
+import petsc4py
+import underworld3 as uw
+import numpy as np
+import sympy
+
+
+
+
+
+
+
+
+
+
+
ls -trl ../Examples-StokesFlow/output/ChannelFlow3D
+
+
+
+
+
+
+
checkpoint_dir = "../Examples-StokesFlow/output/ChannelFlow3D"
+checkpoint_base = f"WigglyBottom_20"
+meshfile = os.path.join(checkpoint_dir, checkpoint_base) + ".mesh.00000.h5"
+
+step = 0
+
+
+
+
+
+
+
uw.utilities.h5_scan("../Examples-StokesFlow/output/ChannelFlow3D/WigglyBottom_20.mesh.P1.00000.h5")
+
+
+
+
+
+
+
terrain_mesh = uw.discretisation.Mesh(meshfile)
+
+v_soln_ckpt = uw.discretisation.MeshVariable("U1", terrain_mesh, terrain_mesh.dim, degree=2)
+p_soln_ckpt = uw.discretisation.MeshVariable("P1", terrain_mesh, 1, degree=1)
+
+v_soln_ckpt.read_timestep(checkpoint_base, "U1", step, outputPath=checkpoint_dir)
+p_soln_ckpt.read_timestep(checkpoint_base, "P1", step, outputPath=checkpoint_dir)
+
+
+
+
+
+
+
## Visualise the mesh
+
+# OR
+# check the mesh if in a notebook / serial
+
+if uw.mpi.size == 1:
+
+    v = v_soln_ckpt
+    p = p_soln_ckpt
+
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(terrain_mesh)
+    pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v.sym)
+
+    velocity_points = vis.meshVariable_to_pv_cloud(v)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v.sym)
+
+    clipped = pvmesh.clip(origin=(0.0, 0.0, -0.09), normal=(0.0, 0, 1), invert=True)
+    clipped.point_data["V"] = vis.vector_fn_to_pv_points(clipped, v.sym)
+
+    clipped2 = pvmesh.clip(origin=(0.0, 0.0, -0.05), normal=(0.0, 0, 1), invert=True)
+    clipped2.point_data["V"] = vis.vector_fn_to_pv_points(clipped2, v.sym)
+    
+    clipped3 = pvmesh.clip(origin=(0.0, 0.0, 0.4), normal=(0.0, 0, 1), invert=False)
+    clipped3.point_data["V"] = vis.vector_fn_to_pv_points(clipped3, v.sym)
+
+
+    skip = 10
+    points = np.zeros((terrain_mesh._centroids[::skip].shape[0], 3))
+    points[:, 0] = terrain_mesh._centroids[::skip, 0]
+    points[:, 1] = terrain_mesh._centroids[::skip, 1]
+    points[:, 2] = terrain_mesh._centroids[::skip, 2]
+
+    point_cloud = pv.PolyData(points[np.logical_and(points[:, 0] < 2.0, points[:, 0] > 0.0)]  )
+
+    pvstream = pvmesh.streamlines_from_source(
+        point_cloud, vectors="V", 
+        integration_direction="forward", 
+        integrator_type=45,
+        surface_streamlines=False,
+        initial_step_length=0.1,
+        max_time=0.5,
+        max_steps=1000
+    )
+
+    point_cloud2 = pv.PolyData(points[np.logical_and(points[:, 2] < 0.5, points[:, 2] > 0.45)]  )
+
+    pvstream2 = pvmesh.streamlines_from_source(
+        point_cloud2, vectors="V", 
+        integration_direction="forward", 
+        integrator_type=45,
+        surface_streamlines=False,
+        initial_step_length=0.01,
+        max_time=0.5,
+        max_steps=1000
+    )
+
+    pl = pv.Plotter(window_size=[1000, 1000])
+    pl.add_axes()
+
+    pl.add_mesh(pvmesh,'Grey', 'wireframe', opacity=0.1)
+    pl.add_mesh(clipped,'Blue', show_edges=False, opacity=0.25)
+    # pl.add_mesh(pvmesh, 'white', show_edges=True, opacity=0.5)
+
+    #pl.add_mesh(pvstream)
+    pl.add_mesh(pvstream2)
+
+
+    arrows = pl.add_arrows(clipped2.points, clipped2.point_data["V"], 
+                           show_scalar_bar = False, opacity=1,
+                           mag=100, )
+    
+    # arrows = pl.add_arrows(clipped3.points, clipped3.point_data["V"], 
+    #                        show_scalar_bar = False, opacity=1,
+    #                        mag=33, )
+
+
+    # pl.screenshot(filename="sphere.png", window_size=(1000, 1000), return_img=False)
+    # OR
+    
+    pl.show(cpos="xy")
+
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-PostProcessing/Ex_DiscStokes_Visualise.html b/main/Notebooks/Examples-PostProcessing/Ex_DiscStokes_Visualise.html new file mode 100644 index 0000000..42bc12c --- /dev/null +++ b/main/Notebooks/Examples-PostProcessing/Ex_DiscStokes_Visualise.html @@ -0,0 +1,761 @@ + + + + + + + + + + + Visualise circular stokes model (flow etc) — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Visualise circular stokes model (flow etc)

+ +
+
+ +
+
+
+ + + + +
+ +
+

Visualise circular stokes model (flow etc)#

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+import os
+
+
+
+
+
+
+
import petsc4py
+import underworld3 as uw
+import numpy as np
+import sympy
+
+
+
+
+
+
+
ls -trl ../Examples-Convection/output/Disc_Ra1e7_H1_deleta_1000.0*T* | tail
+
+
+
+
+
+
+
ls -trl /Users/lmoresi/+Simulations/InnerCore/ConvectionDisk | tail
+
+
+
+
+
+
+
###### checkpoint_dir = "../Examples-Convection/output"
+checkpoint_dir = "/Users/lmoresi/+Simulations/InnerCore/ConvectionDisk"
+checkpoint_base = f"Disc_Ra1e7_H1_deleta_1000.0"
+meshfile = os.path.join(checkpoint_dir, checkpoint_base) + ".mesh.05100.h5"
+
+
+
+
+
+
+
discmesh = uw.discretisation.Mesh(meshfile, 
+                                  coordinate_system_type=uw.coordinates.CoordinateSystemType.CYLINDRICAL2D)
+
+x = discmesh.N.x
+y = discmesh.N.y
+
+r = sympy.sqrt(x**2 + y**2)  # cf radius_fn which is 0->1
+th = sympy.atan2(y + 1.0e-5, x + 1.0e-5)
+
+# swarm = uw.swarm.Swarm(mesh=discmesh)
+v_soln = uw.discretisation.MeshVariable("U", discmesh, discmesh.dim, degree=2)
+t_soln = uw.discretisation.MeshVariable(r"\Delta T", discmesh, 1, degree=2)
+flux = uw.discretisation.MeshVariable(r"dTdz", discmesh, 1, degree=2)
+
+
+
+
+
+
+
# v_soln.read_timestep(checkpoint_base, "U", step, outputPath=checkpoint_dir)
+# t_soln.read_timestep(checkpoint_base, "T", step, outputPath=checkpoint_dir)
+
+
+
+
+
+
+
steps = range(200,6650,10)
+
+
+
+
+
+
+
import mpi4py
+import pyvista as pv
+import underworld3.visualisation as vis
+
+pl = pv.Plotter(window_size=[1000, 1000])
+
+for step in steps:
+
+    try:
+        v_soln.read_timestep(checkpoint_base, "U", step, outputPath=checkpoint_dir)
+        t_soln.read_timestep(checkpoint_base, "T", step, outputPath=checkpoint_dir)
+    except:
+        continue
+
+    pl.clear()
+
+    pvmesh = vis.mesh_to_pv_mesh(discmesh)
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym[0])
+    pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)
+    
+    velocity_points = vis.meshVariable_to_pv_cloud(v_soln)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym)
+    
+    # point sources at cell centres
+    skip = 3
+    points = np.zeros((discmesh._centroids[::skip].shape[0], 3))
+    points[:, 0] = discmesh._centroids[::skip, 0]
+    points[:, 1] = discmesh._centroids[::skip, 1]
+    point_cloud = pv.PolyData(points)
+    
+    pvstream = pvmesh.streamlines_from_source(
+        point_cloud,
+        vectors="V",
+        integration_direction="both",
+        max_time=1,
+        surface_streamlines=True,
+    )
+    
+    
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Grey",
+        show_edges=False,
+        scalars="T",
+        use_transparency=False,
+        opacity=1.0,
+    )
+    
+    pl.add_mesh(pvstream, opacity=0.4, show_scalar_bar=False)
+    pl.add_mesh(pvmesh, "Black", "wireframe",  opacity=0.1)
+    
+    pl.add_points(point_cloud, color="White", point_size=3.0, opacity=0.25)
+    
+    # pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.001, show_scalar_bar=True)
+    
+        
+    # pl.remove_scalar_bar("V")
+    
+    imagefile = os.path.join(checkpoint_dir, checkpoint_base) + f"{step}.png"
+    
+    pl.screenshot(filename=imagefile, window_size=(1000, 1000), return_img=False)
+
+
+
+
+
+
+
+
+
pl.show()
+
+
+
+
+
+
+
uw.systems.Stokes.view()
+
+
+
+
+
+
+
## Calculate heat flux, evaluate at surface — proxy for boundary layer thickness
+
+
+
+
+
+
+
flux_solver = uw.systems.Projection(discmesh, flux)
+
+# Conductive flux only !
+radial_flux = -discmesh.vector.gradient(t_soln.sym[0]).dot(discmesh.CoordinateSystem.unit_e_0)
+radial_flux *= sympy.exp(-100*(r-1)**2)
+
+flux_solver.uw_function = radial_flux
+flux_solver.smoothing = 1.0e-3
+flux_solver.solve()
+
+
+
+
+
+
+
import mpi4py
+import pyvista as pv
+import underworld3.visualisation as vis
+
+pvmesh = vis.mesh_to_pv_mesh(discmesh)
+pvmesh.point_data["dTdz"] = vis.scalar_fn_to_pv_points(pvmesh, flux.sym[0])
+pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)
+pvmesh.point_data["V"] -= pvmesh.point_data["V"].mean()
+
+
+velocity_points = vis.meshVariable_to_pv_cloud(v_soln)
+velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym)
+
+# point sources at cell centres
+skip = 1
+points = np.zeros((discmesh._centroids[::skip].shape[0], 3))
+points[:, 0] = discmesh._centroids[::skip, 0]
+points[:, 1] = discmesh._centroids[::skip, 1]
+point_cloud = pv.PolyData(points)
+
+pvstream = pvmesh.streamlines_from_source(
+    point_cloud,
+    vectors="V",
+    integration_direction="both",
+    max_time=0.5,
+)
+
+pl = pv.Plotter(window_size=[1000, 1000])
+
+pl.add_mesh(
+    pvmesh,
+    cmap="coolwarm",
+    edge_color="Grey",
+    show_edges=True,
+    scalars="dTdz",
+    use_transparency=False,
+    opacity=1.0,
+)
+
+# pl.add_mesh(pvstream, opacity=0.4, show_scalar_bar=False)
+# pl.add_mesh(pvmesh, "Black", "wireframe",  opacity=0.1)
+
+pl.add_points(point_cloud, color="White", point_size=3.0, opacity=0.25)
+
+pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.002, show_scalar_bar=True)
+
+    
+# pl.remove_scalar_bar("V")
+
+imagefile = os.path.join(checkpoint_dir, checkpoint_base) + f"{step}.png"
+
+pl.screenshot(filename=imagefile, window_size=(1000, 1000), return_img=False)
+# OR
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-PostProcessing/Ex_Navier_Stokes_Visualise_Disc.html b/main/Notebooks/Examples-PostProcessing/Ex_Navier_Stokes_Visualise_Disc.html new file mode 100644 index 0000000..9faa3d5 --- /dev/null +++ b/main/Notebooks/Examples-PostProcessing/Ex_Navier_Stokes_Visualise_Disc.html @@ -0,0 +1,840 @@ + + + + + + + + + + + Navier Stokes test: flow in an annulus with a moving boundary (2D) — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Navier Stokes test: flow in an annulus with a moving boundary (2D)

+ +
+
+ +
+
+
+ + + + +
+ + + + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-PostProcessing/Ex_Navier_Stokes_Visualise_NS_DFG_2d.html b/main/Notebooks/Examples-PostProcessing/Ex_Navier_Stokes_Visualise_NS_DFG_2d.html new file mode 100644 index 0000000..f6baf1d --- /dev/null +++ b/main/Notebooks/Examples-PostProcessing/Ex_Navier_Stokes_Visualise_NS_DFG_2d.html @@ -0,0 +1,844 @@ + + + + + + + + + + + Navier Stokes test: flow around a circular inclusion (2D) — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Navier Stokes test: flow around a circular inclusion (2D)

+ +
+
+ +
+
+
+ + + + +
+ + + + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-PostProcessing/Ex_SphericalStokes_Visualise.html b/main/Notebooks/Examples-PostProcessing/Ex_SphericalStokes_Visualise.html new file mode 100644 index 0000000..372ce7f --- /dev/null +++ b/main/Notebooks/Examples-PostProcessing/Ex_SphericalStokes_Visualise.html @@ -0,0 +1,712 @@ + + + + + + + + + + + Visualise spherical stokes model (velocity, particles etc) — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Visualise spherical stokes model (velocity, particles etc)

+ +
+
+ +
+
+
+ + + + +
+ +
+

Visualise spherical stokes model (velocity, particles etc)#

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+import underworld3 as uw
+import numpy as np
+
+
+
+
+
+
+
ls -tr /Users/lmoresi/+Simulations/InnerCore/outputs_free_slip_fk1e-2_ViscGrad0_iic100_QTemp_mr
+
+
+
+
+
+
+
checkpoint_dir = "/Users/lmoresi/+Simulations/InnerCore/outputs_free_slip_fk1e-2_ViscGrad0_iic100_QTemp_mr"
+checkpoint_base = f"free_slip_sphere"
+# basename = f"/Users/lmoresi/+Simulations/InnerCore/outputs_free_slip_fk1e-2_ViscGrad0_iic0_QTemp_mr/free_slip_sphere.h5"
+
+step = 210
+
+res = uw.options.getReal("resolution", default=0.1)
+r_o = uw.options.getReal("radius_o", default=1.0)
+r_i = uw.options.getReal("radius_i", default=0.05)
+
+
+
+
+
+
+
# ls -ltr ~/+Simulations/InnerCore/outputs_free_slip_fk1e-2_ViscGrad0_iic100_QTemp_mr | tail
+
+
+
+
+
+
+
meshball = uw.meshing.SphericalShell(
+    radiusInner=r_i,
+    radiusOuter=r_o,
+    cellSize=res,
+    qdegree=2,
+)
+
+swarm = uw.swarm.Swarm(mesh=meshball)
+v_soln = uw.discretisation.MeshVariable("U", meshball, meshball.dim, degree=2)
+t_soln = uw.discretisation.MeshVariable(r"\Delta T", meshball, 1, degree=2)
+
+
+
+
+
+
+
print(f"Read swarm data", flush=True)
+swarm.load(f"{basename}.passive_swarm.{step}.h5")
+
+
+
+
+
+
+
v_soln.read_timestep(checkpoint_base, "u", 0, outputPath=checkpoint_dir)
+t_soln.read_timestep(checkpoint_base, "deltaT", 0, outputPath=checkpoint_dir)
+
+
+
+
+
+
+
# v_soln.read_timestep
+# t_soln.read_from_vertex_checkpoint(f"{basename}.DeltaT.0.h5", "DeltaT")
+
+
+
+
+
+
+
import mpi4py
+
+if mpi4py.MPI.COMM_WORLD.size == 1:
+
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshball)
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+    pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)
+
+    velocity_points = vis.meshVariable_to_pv_cloud(v_soln)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym)
+    
+    # point sources at cell centres
+    skip = 250
+    points = np.zeros((meshball._centroids[::skip].shape[0], 3))
+    points[:, 0] = meshball._centroids[::skip, 0]
+    points[:, 1] = meshball._centroids[::skip, 1]
+    points[:, 2] = meshball._centroids[::skip, 1]
+    point_cloud = pv.PolyData(points)
+
+    pvstream = pvmesh.streamlines_from_source(
+        point_cloud,
+        vectors="V",
+        integration_direction="both",
+        # max_time=2.0,
+    )
+
+    with swarm.access():
+        points = swarm.data.copy()
+        r2 = points[:,0]**2 + points[:,1]**2 + points[:,2]**2 
+        point_cloud = pv.PolyData(points[r2<0.98**2])
+        # point_cloud.point_data["strain"] = strain.data[:,0]
+
+    sphere = pv.Sphere(radius=0.85, center=(0.0, 0.0, 0.0))
+    clipped = pvmesh.clip_surface(sphere)
+    
+    # clipped = pvmesh.clip(origin=(0.0, 0.0, 0.0), normal=(0.1, 0, 1), invert=True)
+
+        pl = pv.Plotter(window_size=[1000, 1000])
+    # pl.add_axes()
+    
+    pl.camera_position = [(2.1,-4.0,0.0), (0.0,0.0,0.0), (0.0,0.0,1.0)]
+    # pl.camera.
+    # pl.camera.azimuth = -65
+    # pl.camera.distance = 10.0
+    
+    #    pl.camera_position = [(0.00036144256591796875, -0.00045242905616760254, 6.692800318757354),
+    # (0.00036144256591796875, -0.00045242905616760254, 0.00010478496551513672),
+    # (0.0, 1.0, 0.0)]
+
+    pl.add_mesh(
+        clipped,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=False,
+        scalars="T",
+        use_transparency=False,
+        opacity=1.0,
+    )
+
+    pl.add_mesh(pvstream, opacity=0.4)
+    pl.add_mesh(pvmesh, "Black", "wireframe",  opacity=0.1)
+ 
+    # pl.add_mesh(pvmesh, cmap="coolwarm", edge_color="Black", show_edges=True, scalars="T",
+    #               use_transparency=False, opacity=1.0)
+    
+    pl.add_points(point_cloud, color="White", point_size=3.0, opacity=0.25)
+
+    # pl.add_arrows(arrow_loc, arrow_length, mag=20)
+    
+    pl.remove_scalar_bar("T")
+    try:
+        pl.remove_scalar_bar("mag")
+    except KeyError:
+        pass
+    try:
+        pl.remove_scalar_bar("V-normed")
+    except KeyError:
+        pass
+    try:
+        pl.remove_scalar_bar("V")
+    except KeyError:
+        pass
+       
+        
+    # pl.remove_scalar_bar("V")
+
+    pl.screenshot(filename="sphere_iic0.png", window_size=(1000, 1000), return_img=False)
+    # OR
+    pl.show()
+
+
+
+
+
+
+
%%sh
+
+open sphere_iic0.png
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Sandbox-VEP/Ex_Compression_Example.html b/main/Notebooks/Examples-Sandbox-VEP/Ex_Compression_Example.html new file mode 100644 index 0000000..8e1b735 --- /dev/null +++ b/main/Notebooks/Examples-Sandbox-VEP/Ex_Compression_Example.html @@ -0,0 +1,1018 @@ + + + + + + + + + + + Compression / Extension with no mesh deformation — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Compression / Extension with no mesh deformation

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

Compression / Extension with no mesh deformation#

+

This is a rigid inclusion model so it looks a lot like Ex_Shear_Band_Plasticity_PS.py but the geometry is closer to +what we have seen before in various papers.

+

The yield stress is Drucker-Prager / Von Mises (\(\mu\) = 0).

+
+

Examples:#

+

Try \(C = 0.1\) and \(\mu = 0\) to see highly developed shear bands

+

Try \(C = 0.05\) and \(\mu = 0.5\) which does not localise as strongly but is highly non-linear nonetheless.

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+

+C0 = 0.0001
+mu0 = 0.3
+
+expt_name = f"Compression_C{C0}_mu{mu0}"
+
+
+
+
+
+
+
import petsc4py
+import underworld3 as uw
+import numpy as np
+
+
+
+
+
+
+
options = petsc4py.PETSc.Options()
+options["dm_adaptor"] = "pragmatic"
+
+
+
+
+
+
+
import gmsh
+
+# Mesh a 2D pipe with a circular hole
+
+csize = 0.33 # 0.033
+csize_inclusion = 0.02
+res = csize_inclusion
+
+width = 2.0
+height = 1.0
+radius = 0.25
+
+if uw.mpi.rank == 0:
+    # Generate local mesh on boss process
+
+    gmsh.initialize()
+    gmsh.model.add("Notch")
+    gmsh.model.geo.characteristic_length_max = csize
+
+    c0 = gmsh.model.geo.add_point(0.0, 0.0, 0.0, csize_inclusion)
+    cr1 = gmsh.model.geo.add_point(-radius, 0.0, 0.0, csize_inclusion)
+    cr2 = gmsh.model.geo.add_point(0.0, radius, 0.0, csize_inclusion)
+    cr3 = gmsh.model.geo.add_point(+radius, 0.0, 0.0, csize_inclusion)
+    cr4 = gmsh.model.geo.add_point(-radius, radius, 0.0, csize_inclusion)
+    cr5 = gmsh.model.geo.add_point(+radius, radius, 0.0, csize_inclusion)
+
+    cp1 = gmsh.model.geo.add_point(-width, 0.0, 0.0, csize)
+    cp2 = gmsh.model.geo.add_point(+width, 0.0, 0.0, csize)
+    cp3 = gmsh.model.geo.add_point(+width, height, 0.0, csize)
+    cp4 = gmsh.model.geo.add_point(-width, height, 0.0, csize)
+
+    l1 = gmsh.model.geo.add_line(cr3, cp2)
+    l2 = gmsh.model.geo.add_line(cp2, cp3)
+    l3 = gmsh.model.geo.add_line(cp3, cp4)
+    l4 = gmsh.model.geo.add_line(cp4, cp1)
+    l5 = gmsh.model.geo.add_line(cp1, cr1)
+
+    l6 = gmsh.model.geo.add_circle_arc(cr1, c0, cr2)
+    l7 = gmsh.model.geo.add_circle_arc(cr2, c0, cr3)
+    # l6 = gmsh.model.geo.add_line(cr1, cr4)
+    # l7 = gmsh.model.geo.add_line(cr4, cr5)
+    # l8 = gmsh.model.geo.add_line(cr5, cr3)
+
+    cl1 = gmsh.model.geo.add_curve_loop([l1, l2, l3, l4, l5, l6, l7])
+    surf1 = gmsh.model.geo.add_plane_surface(
+        [cl1],
+    )
+
+    gmsh.model.geo.synchronize()
+
+    gmsh.model.add_physical_group(1, [l4], -1, name="Left")
+    gmsh.model.add_physical_group(1, [l2], -1, name="Right")
+    gmsh.model.add_physical_group(1, [l3], -1, name="Top")
+    gmsh.model.add_physical_group(1, [l1, l5], -1, name="FlatBottom")
+    gmsh.model.add_physical_group(1, [l6, l7], -1, name="Hump")
+    gmsh.model.add_physical_group(2, [surf1], -1, name="Elements")
+
+    gmsh.model.mesh.generate(2)
+
+    gmsh.write(f"tmp_hump.msh")
+    gmsh.finalize()
+
+
+
+
+
+
+
mesh1 = uw.discretisation.Mesh("tmp_hump.msh", useRegions=True, simplex=True)
+mesh1.dm.view()
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+
+if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh("tmp_hump.msh")
+
+    pl = pv.Plotter(window_size=(1000, 750))
+
+    points = np.zeros((mesh1._centroids.shape[0], 3))
+    points[:, 0] = mesh1._centroids[:, 0]
+    points[:, 1] = mesh1._centroids[:, 1]
+
+    point_cloud = pv.PolyData(points)
+
+    # pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.5)
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        use_transparency=False,
+        opacity=0.5,
+    )
+
+    #
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
# Define some functions on the mesh
+
+import sympy
+
+# radius_fn = sympy.sqrt(mesh1.rvec.dot(mesh1.rvec)) # normalise by outer radius if not 1.0
+# unit_rvec = mesh1.rvec / (1.0e-10+radius_fn)
+
+# Some useful coordinate stuff
+
+x, y = mesh1.X
+
+# relative to the centre of the inclusion
+r = sympy.sqrt(x**2 + y**2)
+th = sympy.atan2(y, x)
+
+# need a unit_r_vec equivalent
+
+inclusion_rvec = mesh1.X
+inclusion_unit_rvec = inclusion_rvec / inclusion_rvec.dot(inclusion_rvec)
+inclusion_unit_rvec = mesh1.vector.to_matrix(inclusion_unit_rvec)
+
+# Pure shear flow
+
+vx_ps = mesh1.N.x
+vy_ps = -mesh1.N.y
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable("U", mesh1, mesh1.dim, degree=2)
+t_soln = uw.discretisation.MeshVariable("T", mesh1, 1, degree=2)
+p_soln = uw.discretisation.MeshVariable("P", mesh1, 1, degree=1)
+
+vorticity = uw.discretisation.MeshVariable("omega", mesh1, 1, degree=1)
+strain_rate_inv2 = uw.discretisation.MeshVariable("eps", mesh1, 1, degree=1)
+dev_stress_inv2 = uw.discretisation.MeshVariable("tau", mesh1, 1, degree=1)
+node_viscosity = uw.discretisation.MeshVariable("eta", mesh1, 1, degree=1)
+r_inc = uw.discretisation.MeshVariable("R", mesh1, 1, degree=1)
+
+
+
+
+
+
+
# Create Stokes solver object
+
+stokes = uw.systems.Stokes(
+    mesh1,
+    velocityField=v_soln,
+    pressureField=p_soln,
+    verbose=False,
+    solver_name="stokes",
+)
+
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.viscosity = 1
+stokes.saddle_preconditioner = 1 / stokes.constitutive_model.Parameters.viscosity
+stokes.penalty = 0.1
+
+stokes.petsc_options["ksp_monitor"] = None
+stokes.petsc_options["snes_atol"] = 1.0e-4
+
+
+stokes.petsc_options["fieldsplit_velocity_ksp_type"] = "cg"
+stokes.petsc_options["fieldsplit_velocity_pc_type"] = "mg"
+
+stokes.petsc_options["fieldsplit_pressure_ksp_type"] = "gmres"
+stokes.petsc_options["fieldsplit_pressure_pc_type"] = "mg"
+
+
+
+
+
+
+
nodal_strain_rate_inv2 = uw.systems.Projection(
+    mesh1, strain_rate_inv2, solver_name="edot_II"
+)
+nodal_strain_rate_inv2.add_dirichlet_bc(1.0, "Left", 0)
+nodal_strain_rate_inv2.add_dirichlet_bc(1.0, "Right", 0)
+nodal_strain_rate_inv2.uw_function = stokes.Unknowns.Einv2
+nodal_strain_rate_inv2.smoothing = 0.0e-3
+nodal_strain_rate_inv2.petsc_options.delValue("ksp_monitor")
+
+nodal_tau_inv2 = uw.systems.Projection(mesh1, dev_stress_inv2, solver_name="stress_II")
+
+S = stokes.stress_deviator
+nodal_tau_inv2.uw_function = (
+    sympy.simplify(sympy.sqrt(((S**2).trace()) / 2)) - p_soln.sym[0]
+)
+nodal_tau_inv2.smoothing = 0.0e-3
+nodal_tau_inv2.petsc_options.delValue("ksp_monitor")
+
+nodal_visc_calc = uw.systems.Projection(mesh1, node_viscosity, solver_name="visc")
+nodal_visc_calc.uw_function = stokes.constitutive_model.Parameters.viscosity
+nodal_visc_calc.smoothing = 1.0e-3
+nodal_visc_calc.petsc_options.delValue("ksp_monitor")
+
+
+
+
+
+
+
# Set solve options here (or remove default values
+# stokes.petsc_options.getAll()
+
+# Constant visc
+
+stokes.bodyforce = -1 * mesh1.CoordinateSystem.unit_j
+
+hw = 1000.0 / res
+hump_surface_fn = sympy.exp(-(((r - radius) / radius) ** 2) * hw)
+upper_surface_fn = sympy.exp(-(((y - height)) ** 2) * hw)
+
+stokes.bodyforce -= (
+    1.0e6 * hump_surface_fn * v_soln.sym.dot(inclusion_unit_rvec) * inclusion_unit_rvec
+)
+
+# stokes.bodyforce
+p_penalty = 0.0
+stokes.PF0 = p_penalty * upper_surface_fn * p_soln.sym
+stokes.saddle_preconditioner = (
+    1 / stokes.constitutive_model.Parameters.viscosity + p_penalty * upper_surface_fn
+)
+
+# Velocity boundary conditions
+
+# stokes.add_dirichlet_bc((0.0, 0.0), "Hump", (0, 1))
+# stokes.add_dirichlet_bc((vx_ps, vy_ps), ["top", "bottom", "left", "right"], (0, 1))
+stokes.add_dirichlet_bc((1.0, 0.0), "Left", (0, 1))
+stokes.add_dirichlet_bc((-1.0, 0.0), "Right", (0, 1))
+stokes.add_dirichlet_bc((0.0,), "FlatBottom", (1,))
+
+
+
+
+
+
+
# linear solve first
+
+stokes.solve(zero_init_guess=False)
+
+
+
+
+
+
+
# Calculate surface pressure
+
+_, _, _, _, ps_sum, _, _ = mesh1.stats(p_soln.sym[0] * upper_surface_fn, p_soln)
+_, _, _, _, p_sum, _, _ = mesh1.stats(p_soln.sym[0], p_soln)
+_, _, _, _, ps_norm, _, _ = mesh1.stats(upper_surface_fn, p_soln)
+_, _, _, _, p_norm, _, _ = mesh1.stats(1 + 0.00001 * p_soln.sym[0], p_soln)
+
+print(f"Mean Surface P - {ps_sum/p_sum}")
+print(f"Mean P - {p_sum/p_norm}")
+
+
+# p_calculator = uw.maths.Integral(mesh1, p_soln.sym[0] * upper_surface_fn)
+# value = p_calculator.evaluate()
+
+# # calculator.fn = upper_surface_fn
+# # norm = calculator.evaluate()
+
+# integral = value # / norm
+
+# print(f"Average surface pressure: {integral}")
+
+
+
+
+
+
+

+# stokes.solve(zero_init_guess=False)
+
+
+
+
+
+
+
# Approach the required value by shifting the parameters
+
+for i in range(1):
+    mu = mu0
+    C = C0  # + (1 - i / 4) * 0.1
+    print(f"Mu - {mu}, C = {C}")
+    tau_y = sympy.Max(
+        C + mu * stokes.p.sym[0] + 1 * sympy.sin(x * sympy.pi / (2 * width)) ** 2,
+        0.0001,
+    )
+    viscosity = 1.0 / (2 * stokes.Unknowns.Einv2 / tau_y + 1.0)
+
+    stokes.constitutive_model.Parameters.viscosity = viscosity
+    stokes.saddle_preconditioner = (
+        1 / stokes.constitutive_model.Parameters.viscosity
+        + p_penalty * upper_surface_fn
+    )
+    stokes.solve(zero_init_guess=False)
+
+
+
+
+
+
+
nodal_tau_inv2.uw_function = (
+    stokes.constitutive_model.Parameters.viscosity * stokes.Unknowns.Einv2
+)
+nodal_tau_inv2.solve()
+nodal_visc_calc.uw_function = stokes.constitutive_model.Parameters.viscosity
+nodal_visc_calc.solve()
+nodal_strain_rate_inv2.solve()
+
+
+
+
+
+
+
mesh1.petsc_save_checkpoint(index=0, meshVars=[v_soln, p_soln, dev_stress_inv2, strain_rate_inv2, node_viscosity], 
+                            outputPath="./output/")
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+
+if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+    
+    pvmesh = vis.mesh_to_pv_mesh(mesh1)
+    pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym)
+    pvmesh.point_data["Edot"] = vis.scalar_fn_to_pv_points(pvmesh, strain_rate_inv2.sym)
+    pvmesh.point_data["Visc"] = vis.scalar_fn_to_pv_points(pvmesh, node_viscosity.sym)
+    pvmesh.point_data["Str"] = vis.scalar_fn_to_pv_points(pvmesh, dev_stress_inv2.sym)
+    pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)
+    pvmesh.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh, v_soln.sym.dot(v_soln.sym))
+
+    velocity_points = vis.meshVariable_to_pv_cloud(v_soln)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym)
+
+
+    # point sources at cell centres
+    points = np.zeros((mesh1._centroids.shape[0], 3))
+    points[:, 0] = mesh1._centroids[:, 0]
+    points[:, 1] = mesh1._centroids[:, 1]
+    point_cloud = pv.PolyData(points)
+
+    pvstream = pvmesh.streamlines_from_source(
+        point_cloud, vectors="V", integration_direction="both", max_steps=100
+    )
+
+    pl = pv.Plotter(window_size=(1000, 750))
+
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.05, opacity=0.75)
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        scalars="Edot",
+        use_transparency=False,
+        opacity=1.0,
+        clim=[0.0, 4.0],
+    )
+
+    # pl.remove_scalar_bar("mag")
+
+    pl.show()
+
+
+
+
+
+
+
if uw.mpi.size == 1:
+    print(pvmesh.point_data["Visc"].min(), pvmesh.point_data["Visc"].max())
+
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Sandbox-VEP/Ex_Shear_Band_Notch_Benchmark.html b/main/Notebooks/Examples-Sandbox-VEP/Ex_Shear_Band_Notch_Benchmark.html new file mode 100644 index 0000000..f628b5a --- /dev/null +++ b/main/Notebooks/Examples-Sandbox-VEP/Ex_Shear_Band_Notch_Benchmark.html @@ -0,0 +1,1341 @@ + + + + + + + + + + + Spiegelman et al, notch-deformation benchmark — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Spiegelman et al, notch-deformation benchmark

+ +
+ +
+
+ + + + +
+ +
+

Spiegelman et al, notch-deformation benchmark#

+

This example is for the notch-localization test of Spiegelman et al. For which they supply a geometry file which gmsh can use to construct meshes at various resolutions. NOTE: we are just demonstrating the mesh here, not the solver configuration / benchmarking.

+

The .geo file is provided and we show how to make this into a .msh file and +how to read that into a uw.discretisation.Mesh object. The .geo file has header parameters to control the mesh refinement, and we provide a coarse version and the original version.

+

After that, there is some cell data which we can assign to a data structure on the elements (such as a swarm).

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+import underworld3 as uw
+import numpy as np
+import sympy
+import gmsh
+import os
+
+os.makedirs("meshes", exist_ok=True)
+
+if uw.mpi.size == 1:
+    os.makedirs("output", exist_ok=True)
+else:
+    os.makedirs(f"output_np{uw.mpi.size}", exist_ok=True)
+
+
+os.environ["UW_TIMING_ENABLE"] = "1"
+
+# Define the problem size
+#      1 - ultra low res for automatic checking
+#      2 - low res problem to play with this notebook
+#      3 - medium resolution (be prepared to wait)
+#      4 - highest resolution (benchmark case from Spiegelman et al)
+
+problem_size = 2
+
+# For testing and automatic generation of notebook output,
+# over-ride the problem size if the UW_TESTING_LEVEL is set
+
+uw_testing_level = os.environ.get("UW_TESTING_LEVEL")
+if uw_testing_level:
+    try:
+        problem_size = int(uw_testing_level)
+    except ValueError:
+        # Accept the default value
+        pass
+
+
+
+
+
+
+
from underworld3.cython import petsc_discretisation
+
+
+
+
+
+
+
if problem_size <= 1:
+    cl_1 = 0.25
+    cl_2 = 0.15
+    cl_2a = 0.1
+    cl_3 = 0.25
+    cl_4 = 0.15
+elif problem_size == 2:
+    cl_1 = 0.1
+    cl_2 = 0.05
+    cl_2a = 0.03
+    cl_3 = 0.1
+    cl_4 = 0.05
+elif problem_size == 3:
+    cl_1 = 0.06
+    cl_2 = 0.03
+    cl_2a = 0.015
+    cl_3 = 0.04
+    cl_4 = 0.02
+else:
+    cl_1 = 0.04
+    cl_2 = 0.005
+    cl_2a = 0.003
+    cl_3 = 0.02
+    cl_4 = 0.01
+
+# The benchmark provides a .geo file. This is the gmsh python
+# equivalent (mostly transcribed from the .geo format). The duplicated
+# Point2 caused a few problems with the mesh reader at one point.
+
+if uw.mpi.rank == 0:
+    gmsh.initialize()
+    gmsh.option.setNumber("General.Verbosity", 0)
+    gmsh.model.add("Notch")
+
+    Point1 = gmsh.model.geo.addPoint(-2, -1, 0, cl_1)
+    # Point2 = gmsh.model.geo.addPoint(-2, -1, 0, cl_1)
+    Point3 = gmsh.model.geo.addPoint(+2, -1, 0, cl_1)
+    Point4 = gmsh.model.geo.addPoint(2, -0.75, 0, cl_1)
+    Point5 = gmsh.model.geo.addPoint(2, 0, 0, cl_1)
+    Point6 = gmsh.model.geo.addPoint(-2, 0, 0, cl_1)
+    Point7 = gmsh.model.geo.addPoint(-2, -0.75, 0, cl_1)
+    Point8 = gmsh.model.geo.addPoint(-0.08333333333329999, -0.75, 0, cl_2)
+    Point9 = gmsh.model.geo.addPoint(0.08333333333329999, -0.75, 0, cl_2)
+    Point10 = gmsh.model.geo.addPoint(0.08333333333329999, -0.6666666666667, 0, cl_2)
+    Point11 = gmsh.model.geo.addPoint(-0.08333333333329999, -0.6666666666667, 0, cl_2)
+    Point25 = gmsh.model.geo.addPoint(-0.75, 0, 0, cl_4)
+    Point26 = gmsh.model.geo.addPoint(0.75, 0, 0, cl_4)
+    Point27 = gmsh.model.geo.addPoint(0, 0, 0, cl_3)
+
+    Line1 = gmsh.model.geo.addLine(Point1, Point3)
+    Line2 = gmsh.model.geo.addLine(Point3, Point4)
+    Line3 = gmsh.model.geo.addLine(Point4, Point5)
+    Line4 = gmsh.model.geo.addLine(Point5, Point26)
+    Line8 = gmsh.model.geo.addLine(Point26, Point27)
+    Line9 = gmsh.model.geo.addLine(Point27, Point25)
+    Line10 = gmsh.model.geo.addLine(Point25, Point6)
+    Line6 = gmsh.model.geo.addLine(Point6, Point7)
+    Line7 = gmsh.model.geo.addLine(Point7, Point1)
+
+    Point12 = gmsh.model.geo.addPoint(-0.1033333333333, -0.75, 0, cl_2a)
+    Point13 = gmsh.model.geo.addPoint(-0.0833333333333, -0.73, 0, cl_2a)
+    Point14 = gmsh.model.geo.addPoint(-0.0833333333333, -0.686666666666666, 0, cl_2a)
+    Point15 = gmsh.model.geo.addPoint(-0.0633333333333, -0.666666666666666, 0, cl_2a)
+    Point16 = gmsh.model.geo.addPoint(0.0633333333333, -0.666666666666666, 0, cl_2a)
+    Point17 = gmsh.model.geo.addPoint(0.0833333333333, -0.686666666666666, 0, cl_2a)
+    Point18 = gmsh.model.geo.addPoint(0.0833333333333, -0.73, 0, cl_2a)
+    Point19 = gmsh.model.geo.addPoint(0.1033333333333, -0.75, 0, cl_2a)
+    Point20 = gmsh.model.geo.addPoint(-0.103333333333333, -0.73, 0, cl_2a)
+    Point21 = gmsh.model.geo.addPoint(-0.063333333333333, -0.686666666666666, 0, cl_2a)
+    Point22 = gmsh.model.geo.addPoint(0.063333333333333, -0.686666666666666, 0, cl_2a)
+    Point24 = gmsh.model.geo.addPoint(0.103333333333333, -0.73, 0, cl_2a)
+
+    Circle22 = gmsh.model.geo.addCircleArc(Point12, Point20, Point13)
+    Circle23 = gmsh.model.geo.addCircleArc(Point14, Point21, Point15)
+    Circle24 = gmsh.model.geo.addCircleArc(Point16, Point22, Point17)
+    Circle25 = gmsh.model.geo.addCircleArc(Point18, Point24, Point19)
+
+    Line26 = gmsh.model.geo.addLine(Point7, Point12)
+    Line27 = gmsh.model.geo.addLine(Point13, Point14)
+    Line28 = gmsh.model.geo.addLine(Point15, Point16)
+    Line29 = gmsh.model.geo.addLine(Point17, Point18)
+    Line30 = gmsh.model.geo.addLine(Point19, Point4)
+
+    LineLoop31 = gmsh.model.geo.addCurveLoop(
+        [
+            Line1,
+            Line2,
+            -Line30,
+            -Circle25,
+            -Line29,
+            -Circle24,
+            -Line28,
+            -Circle23,
+            -Line27,
+            -Circle22,
+            -Line26,
+            Line7,
+        ],
+    )
+
+    LineLoop33 = gmsh.model.geo.addCurveLoop(
+        [
+            Line6,
+            Line26,
+            Circle22,
+            Line27,
+            Circle23,
+            Line28,
+            Circle24,
+            Line29,
+            Circle25,
+            Line30,
+            Line3,
+            Line4,
+            Line8,
+            Line9,
+            Line10,
+        ],
+    )
+
+    Surface32 = gmsh.model.geo.addPlaneSurface([LineLoop31])
+    Surface34 = gmsh.model.geo.addPlaneSurface([LineLoop33])
+
+    gmsh.model.geo.synchronize()
+
+    gmsh.model.addPhysicalGroup(1, [Line1], tag=3, name="Bottom")
+    gmsh.model.addPhysicalGroup(1, [Line2, Line3], tag=2, name="Right")
+    gmsh.model.addPhysicalGroup(1, [Line7, Line6], tag=1, name="Left")
+    gmsh.model.addPhysicalGroup(1, [Line4, Line8, Line9, Line10], tag=4, name="Top")
+
+    gmsh.model.addPhysicalGroup(
+        1,
+        [
+            Line26,
+            Circle22,
+            Line27,
+            Circle23,
+            Line28,
+            Circle24,
+            Line29,
+            Circle25,
+            Line30,
+        ],
+        tag=5,
+        name="InnerBoundary",
+    )
+
+    gmsh.model.addPhysicalGroup(2, [Surface32], tag=100, name="Weak")
+    gmsh.model.addPhysicalGroup(2, [Surface34], tag=101, name="Strong")
+
+    gmsh.model.mesh.generate(2)
+
+    gmsh.write(f"./meshes/notch_mesh{problem_size}.msh")
+    gmsh.finalize()
+
+
+
+
+
+
+
from underworld3 import timing
+
+
+
+
+
+
+
timing.reset()
+timing.start()
+
+
+
+
+
+
+
mesh1 = uw.discretisation.Mesh(
+    f"./meshes/notch_mesh{problem_size}.msh",
+    simplex=True,
+    qdegree=3,
+    markVertices=False,
+    useRegions=True,
+    useMultipleTags=True,
+)
+
+
+
+
+
+
+
if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh1)
+
+    pl = pv.Plotter(window_size=(1000, 750))
+
+    pl.add_mesh(
+        pvmesh,
+        "Blue",
+        "wireframe",
+        opacity=0.5,
+    )
+    # pl.add_points(point_cloud, cmap="coolwarm", render_points_as_spheres=False, point_size=10, opacity=0.66)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
swarm = uw.swarm.Swarm(mesh=mesh1)
+material = uw.swarm.SwarmVariable(
+    "M", swarm, size=1, proxy_continuous=False, proxy_degree=0
+)
+swarm.populate(fill_param=0)
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable(r"U", mesh1, mesh1.dim, degree=2)
+p_soln = uw.discretisation.MeshVariable(r"P", mesh1, 1, degree=1, continuous=True)
+p_null = uw.discretisation.MeshVariable(r"P2", mesh1, 1, degree=1, continuous=True)
+
+
+
+
+
+
+
edot = uw.discretisation.MeshVariable(
+    r"\dot\varepsilon", mesh1, 1, degree=1, continuous=True
+)
+visc = uw.discretisation.MeshVariable(r"\eta", mesh1, 1, degree=1, continuous=False)
+stress = uw.discretisation.MeshVariable(r"\sigma", mesh1, 1, degree=1, continuous=True)
+
+
+
+
+

This is how we extract cell data from the mesh. We can map it to the swarm data structure and use this to +build material properties that depend on cell type.

+
+
+
indexSetW = mesh1.dm.getStratumIS("Weak", 100)
+indexSetS = mesh1.dm.getStratumIS("Strong", 101)
+
+
+
+
+
+
+
l = swarm.dm.createLocalVectorFromField("M")
+lvec = l.copy()
+swarm.dm.restoreField("M")
+
+
+
+
+
+
+
lvec.isset(indexSetW, 0.0)
+lvec.isset(indexSetS, 1.0)
+
+
+
+
+
+
+
with swarm.access(material):
+    material.data[:, 0] = lvec.array[:]
+
+
+
+
+

check the mesh if in a notebook / serial

+
+
+
if True and uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(f"./meshes/notch_mesh{problem_size}.msh")
+    pvmesh.point_data["eta"] = vis.scalar_fn_to_pv_points(pvmesh, material.sym)
+
+    pl = pv.Plotter(window_size=(1000, 750))
+
+    # points = np.zeros((mesh1._centroids.shape[0], 3))
+    # points[:, 0] = mesh1._centroids[:, 0]
+    # points[:, 1] = mesh1._centroids[:, 1]
+
+    points = vis.swarm_to_pv_cloud(swarm)
+    point_cloud = pv.PolyData(points)
+
+    with swarm.access():
+        point_cloud.point_data["M"] = material.data.copy()
+
+
+    # pl.add_mesh(
+    #     pvmesh,
+    #     cmap="coolwarm",
+    #     edge_color="Black",
+    #     show_edges=True,
+    #     use_transparency=False,
+    #     opacity=0.5,
+    # )
+    
+    pl.add_points(
+        point_cloud,
+        cmap="coolwarm",
+        render_points_as_spheres=False,
+        point_size=10,
+        opacity=0.66,
+    )
+    pl.add_mesh(pvmesh, "Black", "wireframe")
+
+    pl.show(cpos="xy")
+
+
+
+
+
+

Check that this mesh can be solved for a simple, linear problem#

+

Create Stokes object

+
+
+
stokes = uw.systems.Stokes(
+    mesh1,
+    velocityField=v_soln,
+    pressureField=p_soln,
+    solver_name="stokes",
+    verbose=False,
+)
+
+
+
+
+
+
+
# Set solve options here (or remove default values
+stokes.petsc_options["ksp_monitor"] = None
+
+stokes.tolerance = 1.0e-6
+stokes.petsc_options["snes_atol"] = 1e-2
+stokes.bodyforce = sympy.Matrix([0, -0.001]).T
+
+# stokes.petsc_options["fieldsplit_velocity_ksp_rtol"] = 1e-4
+# stokes.petsc_options["fieldsplit_pressure_ksp_type"] = "gmres" # gmres here for bulletproof
+stokes.petsc_options[
+    "fieldsplit_pressure_pc_type"
+] = "gamg"  # can use gasm / gamg / lu here
+stokes.petsc_options[
+    "fieldsplit_pressure_pc_gasm_type"
+] = "basic"  # can use gasm / gamg / lu here
+stokes.petsc_options[
+    "fieldsplit_pressure_pc_gamg_type"
+] = "classical"  # can use gasm / gamg / lu here
+stokes.petsc_options["fieldsplit_pressure_pc_gamg_classical_type"] = "direct"
+stokes.petsc_options["fieldsplit_pressure_pc_gamg_esteig_ksp_type"] = "cg"
+
+
+
+
+
+
+
stokes.constitutive_model
+
+
+
+
+
+
+
viscosity_L = 999.0 * material.sym[0] + 1.0
+
+
+
+
+
+
+
stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.viscosity = viscosity_L
+stokes.saddle_preconditioner = 1 / viscosity_L
+stokes.penalty = 0.1
+
+
+
+
+
+
+
# Velocity boundary conditions
+stokes.add_dirichlet_bc(1.0, "Left", 0)
+stokes.add_dirichlet_bc(0, "Left", 1)
+stokes.add_dirichlet_bc(-1.0, "Right", 0)
+stokes.add_dirichlet_bc(0, "Right", 1)
+stokes.add_dirichlet_bc((0.0,), "Bottom", (1,))
+# stokes.add_dirichlet_bc((0.0,), "Top", (1,))
+
+
+
+
+
+
+
stokes.bodyforce = sympy.Matrix([0, -1])
+
+
+
+
+
+
+
x, y = mesh1.X
+
+res = 0.1
+hw = 1000.0 / res
+surface_defn_fn = sympy.exp(-((y - 0) ** 2) * hw)
+base_defn_fn = sympy.exp(-((y + 1) ** 2) * hw)
+edges_fn = sympy.exp(-((x - 2) ** 2) / 0.025) + sympy.exp(-((x + 2) ** 2) / 0.025)
+# stokes.bodyforce -= 10000.0 * surface_defn_fn * v_soln.sym[1] * mesh1.CoordinateSystem.unit_j
+
+
+
+
+
+
+
stokes.constitutive_model
+
+
+
+
+

This is a strategy to obtain integrals over the surface (etc)

+
+
+
def surface_integral(mesh, uw_function, mask_fn):
+    calculator = uw.maths.Integral(mesh, uw_function * mask_fn)
+    value = calculator.evaluate()
+
+    calculator.fn = mask_fn
+    norm = calculator.evaluate()
+
+    integral = value / norm
+
+    return integral
+
+
+
+
+
+
+
# %%
+strain_rate_calc = uw.systems.Projection(mesh1, edot)
+strain_rate_calc.uw_function = stokes.Unknowns.Einv2
+strain_rate_calc.smoothing = 1.0e-3
+
+
+
+
+
+
+
viscosity_calc = uw.systems.Projection(mesh1, visc)
+viscosity_calc.uw_function = stokes.constitutive_model.Parameters.viscosity
+viscosity_calc.smoothing = 1.0e-3
+
+
+
+
+
+
+
stress_calc = uw.systems.Projection(mesh1, stress)
+S = stokes.stress_deviator
+stress_calc.uw_function = (
+    sympy.simplify(sympy.sqrt(((S**2).trace()) / 2)) - p_soln.sym[0]
+)
+stress_calc.smoothing = 1.0e-3
+
+
+
+
+
+
+
# stokes._setup_terms()
+
+
+
+
+
+
+
# stokes._uu_G3
+
+
+
+
+
+
+
# First, we solve the linear problem
+
+stokes.tolerance = 1e-4
+stokes.petsc_options["snes_atol"] = 1.0e-2
+
+# stokes.petsc_options["ksp_rtol"]  = 1.0e-4
+# stokes.petsc_options["ksp_atol"]  = 1.0e-8
+
+# stokes.petsc_options["fieldsplit_pressure_ksp_rtol"]  = 1.0e-5
+# stokes.petsc_options["fieldsplit_velocity_ksp_rtol"]  = 1.0e-5
+
+
+stokes.solve(zero_init_guess=True)
+
+if uw.mpi.rank == 0:
+    print("Linear solve complete", flush=True)
+
+
+
+
+
+
+

+C0 = 150
+for i in range(1,10,2):
+    mu = 0.75
+    C = C0 + (1.0 - i / 9) * 15.0
+    if uw.mpi.rank == 0:
+        print(f"Mu - {mu}, C = {C}", flush=True)
+
+    tau_y = C + mu * p_soln.sym[0]
+    viscosity_L = 999.0 * material.sym[0] + 1.0
+    viscosity_Y = tau_y / (2 * stokes.Unknowns.Einv2 + 1.0 / 1000)
+    viscosity = 1 / (1 / viscosity_Y + 1 / viscosity_L)
+
+    stokes.constitutive_model.Parameters.shear_viscosity_0 = viscosity
+    stokes.saddle_preconditioner = 1 / viscosity
+
+    # +
+    # Now use that as the guess for a better job
+
+    # stokes.tolerance = 1e-4
+    # stokes.petsc_options["ksp_rtol"]  = 1.0e-4
+    # stokes.petsc_options["ksp_atol"]  = 1.0e-8
+
+    # stokes.petsc_options["fieldsplit_pressure_ksp_rtol"]  = 1.0e-5
+    # stokes.petsc_options["fieldsplit_velocity_ksp_rtol"]  = 1.0e-5
+    # stokes.snes.atol = 1e-3
+
+    stokes.solve(zero_init_guess=False)
+    if uw.mpi.rank == 0:
+        print(f"Completed: Mu - {mu}, C = {C}", flush=True)
+
+
+
+
+
+
+
# %%
+viscosity_calc.uw_function = stokes.constitutive_model.Parameters.viscosity
+stress_calc.uw_function = (
+    2 * stokes.constitutive_model.Parameters.viscosity * stokes.Unknowns.Einv2
+)
+
+
+
+
+
+
+
# %%
+strain_rate_calc.solve()
+viscosity_calc.solve()
+stress_calc.solve()
+
+
+
+
+
+
+
stress.stats()
+
+
+
+
+
+
+
## Save data ...
+savefile = f"output/notched_beam_mesh_{problem_size}"
+mesh1.petsc_save_checkpoint(index=0, meshVars=[p_soln, v_soln, edot], outputPath=savefile)
+
+
+
+
+

check the mesh if in a notebook / serial

+
+
+
if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh1)
+    pvmesh.point_data["sfn"] = vis.scalar_fn_to_pv_points(pvmesh, surface_defn_fn)
+    pvmesh.point_data["pres"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym)
+    pvmesh.point_data["edot"] = vis.scalar_fn_to_pv_points(pvmesh, edot.sym)
+    pvmesh.point_data["eta"] = vis.scalar_fn_to_pv_points(pvmesh, visc.sym)
+    pvmesh.point_data["str"] = vis.scalar_fn_to_pv_points(pvmesh, stress.sym)
+
+    velocity_points = vis.meshVariable_to_pv_cloud(v_soln)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym)
+    
+    points = np.zeros((mesh1._centroids.shape[0], 3))
+    points[:, 0] = mesh1._centroids[:, 0]
+    points[:, 1] = mesh1._centroids[:, 1]
+    point_cloud = pv.PolyData(points)
+
+    with swarm.access():
+        point_cloud.point_data["M"] = material.data.copy()
+
+    pl = pv.Plotter(window_size=(1000, 750))
+
+    # pl.add_arrows(arrow_loc, arrow_length, mag=0.03, opacity=0.75)
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="RdYlGn",
+        scalars="eta",
+        edge_color="Grey",
+        show_edges=True,
+        use_transparency=False,
+        clim=[0.1, 1.5],
+        opacity=1.0,
+    )
+
+    # pl.add_points(
+    #     point_cloud,
+    #     cmap="coolwarm",
+    #     render_points_as_spheres=False,
+    #     point_size=5,
+    #     opacity=0.1,
+    # )
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
0/0
+
+
+
+
+
+
+
# %%
+# surface_defn_fn = sympy.exp(-((y - 0) ** 2) * hw)
+# p_surface_ave = surface_integral(mesh1, p_soln.sym[0], surface_defn_fn)
+# print(f"Upper surface average P = {p_surface_ave}")
+
+
+
+
+
+
+
# surface_defn_fn = sympy.exp(-((y + 1) ** 2) * hw)
+# p_surface_ave = surface_integral(mesh1, p_soln.sym[0], surface_defn_fn)
+# print(f"Lower surface average P = {p_surface_ave}")
+
+
+
+
+
+
+
# %%
+# surface_defn_fn = sympy.exp(-((y + 0.666) ** 2) * hw)
+# p_surface_ave = surface_integral(mesh1, edot.sym[0], surface_defn_fn)
+# print(f"Edot at 0.666 = {p_surface_ave}")
+
+
+
+
+
+
+
if uw.mpi.size == 1:
+    print(pvmesh.point_data["eta"].min(), pvmesh.point_data["eta"].max())
+
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Sandbox-VEP/Ex_Shear_Band_Plasticity_PS.html b/main/Notebooks/Examples-Sandbox-VEP/Ex_Shear_Band_Plasticity_PS.html new file mode 100644 index 0000000..0334710 --- /dev/null +++ b/main/Notebooks/Examples-Sandbox-VEP/Ex_Shear_Band_Plasticity_PS.html @@ -0,0 +1,924 @@ + + + + + + + + + + + Flow and Shear banding around a circular inclusion in pure shear — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Flow and Shear banding around a circular inclusion in pure shear

+ +
+
+ +
+
+
+ + + + +
+ +
+

Flow and Shear banding around a circular inclusion in pure shear#

+

Masuda, T., & Mizuno, N. (1995). Deflection of pure shear viscous flow around a rigid spherical body. Journal of Structural Geology, 17(11), 1615–1620. https://doi.org/10.1016/0191-8141(95)E0016-6

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
expt_name = "PS_ShearBand"
+
+
+
+
+
+
+
import petsc4py
+import underworld3 as uw
+import numpy as np
+
+
+
+
+
+
+
import meshio, pygmsh
+
+# Mesh a 2D pipe with a circular hole
+
+csize = 0.075
+csize_circle = 0.025
+res = csize_circle
+
+width = 1.0
+height = 1.0
+radius = 0.2
+
+if uw.mpi.rank == 0:
+    # Generate local mesh on boss process
+
+    with pygmsh.geo.Geometry() as geom:
+        geom.characteristic_length_max = csize
+
+        inclusion = geom.add_circle(
+            (0.0, 0.0, 0.0), radius, make_surface=False, mesh_size=csize_circle
+        )
+        domain = geom.add_rectangle(
+            xmin=-width,
+            ymin=-height,
+            xmax=width,
+            ymax=height,
+            z=0,
+            holes=[inclusion],
+            mesh_size=csize,
+        )
+
+        geom.add_physical(domain.surface.curve_loop.curves[0], label="bottom")
+        geom.add_physical(domain.surface.curve_loop.curves[1], label="right")
+        geom.add_physical(domain.surface.curve_loop.curves[2], label="top")
+        geom.add_physical(domain.surface.curve_loop.curves[3], label="left")
+
+        geom.add_physical(inclusion.curve_loop.curves, label="inclusion")
+
+        geom.add_physical(domain.surface, label="Elements")
+
+        geom.generate_mesh(dim=2, verbose=False)
+        geom.save_geometry("tmp_ps_shear_inclusion.msh")
+
+
+
+
+
+
+
mesh1 = uw.discretisation.Mesh(
+    "tmp_ps_shear_inclusion.msh", markVertices=True, useRegions=True, simplex=True
+)
+mesh1.dm.view()
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+
+if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh1)
+
+    pl = pv.Plotter(window_size=(1000, 750))
+
+    points = np.zeros((mesh1._centroids.shape[0], 3))
+    points[:, 0] = mesh1._centroids[:, 0]
+    points[:, 1] = mesh1._centroids[:, 1]
+
+    point_cloud = pv.PolyData(points)
+
+    # pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.5)
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        use_transparency=False,
+        opacity=0.5,
+    )
+
+    #
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
# Define some functions on the mesh
+
+import sympy
+
+# radius_fn = sympy.sqrt(mesh1.rvec.dot(mesh1.rvec)) # normalise by outer radius if not 1.0
+# unit_rvec = mesh1.rvec / (1.0e-10+radius_fn)
+
+# Some useful coordinate stuff
+
+x, y = mesh1.X
+
+# relative to the centre of the inclusion
+r = sympy.sqrt(x**2 + y**2)
+th = sympy.atan2(y, x)
+
+# need a unit_r_vec equivalent
+
+inclusion_rvec = mesh1.X
+inclusion_unit_rvec = inclusion_rvec / inclusion_rvec.dot(inclusion_rvec)
+inclusion_unit_rvec = mesh1.vector.to_matrix(inclusion_unit_rvec)
+
+# Pure shear flow
+
+vx_ps = mesh1.N.x
+vy_ps = -mesh1.N.y
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable("U", mesh1, mesh1.dim, degree=2)
+t_soln = uw.discretisation.MeshVariable("T", mesh1, 1, degree=2)
+p_soln = uw.discretisation.MeshVariable("P", mesh1, 1, degree=1)
+
+vorticity = uw.discretisation.MeshVariable("omega", mesh1, 1, degree=1)
+strain_rate_inv2 = uw.discretisation.MeshVariable("eps", mesh1, 1, degree=1)
+dev_stress_inv2 = uw.discretisation.MeshVariable("tau", mesh1, 1, degree=1)
+node_viscosity = uw.discretisation.MeshVariable("eta", mesh1, 1, degree=1)
+r_inc = uw.discretisation.MeshVariable("R", mesh1, 1, degree=1)
+
+
+
+
+
+
+
# Create NS object
+
+stokes = uw.systems.Stokes(
+    mesh1,
+    velocityField=v_soln,
+    pressureField=p_soln,
+    verbose=False,
+    solver_name="stokes",
+)
+
+
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.viscosity = 1
+stokes.saddle_preconditioner = 1 / stokes.constitutive_model.Parameters.viscosity
+stokes.penalty = 0.0
+
+stokes.petsc_options["ksp_monitor"] = None
+
+
+
+
+
+
+
nodal_strain_rate_inv2 = uw.systems.Projection(
+    mesh1, strain_rate_inv2, solver_name="edot_II"
+)
+nodal_strain_rate_inv2.add_dirichlet_bc(1.0, "top", 0)
+nodal_strain_rate_inv2.add_dirichlet_bc(1.0, "bottom", 0)
+nodal_strain_rate_inv2.uw_function = stokes.Unknowns.Einv2
+nodal_strain_rate_inv2.smoothing = 1.0e-3
+nodal_strain_rate_inv2.petsc_options.delValue("ksp_monitor")
+
+nodal_tau_inv2 = uw.systems.Projection(mesh1, dev_stress_inv2, solver_name="stress_II")
+
+S = stokes.stress_deviator
+nodal_tau_inv2.uw_function = (
+    sympy.simplify(sympy.sqrt(((S**2).trace()) / 2)) - p_soln.sym[0]
+)
+nodal_tau_inv2.smoothing = 1.0e-3
+nodal_tau_inv2.petsc_options.delValue("ksp_monitor")
+
+nodal_visc_calc = uw.systems.Projection(mesh1, node_viscosity, solver_name="visc")
+nodal_visc_calc.uw_function = stokes.constitutive_model.Parameters.viscosity
+nodal_visc_calc.smoothing = 1.0e-3
+nodal_visc_calc.petsc_options.delValue("ksp_monitor")
+
+
+
+
+
+
+
# Set solve options here (or remove default values
+# stokes.petsc_options.getAll()
+
+# Constant visc
+
+stokes.penalty = 0.0
+stokes.bodyforce = 1.0e-32 * mesh1.N.i
+
+hw = 1000.0 / res
+surface_defn_fn = sympy.exp(-(((r - radius) / radius) ** 2) * hw)
+stokes.bodyforce -= (
+    1.0e6 * surface_defn_fn * v_soln.sym.dot(inclusion_unit_rvec) * inclusion_unit_rvec
+)
+
+# Velocity boundary conditions
+
+# stokes.add_dirichlet_bc((0.0, 0.0), "inclusion", (0, 1))
+stokes.add_dirichlet_bc((vx_ps, vy_ps), "top", (0, 1))
+stokes.add_dirichlet_bc((vx_ps, vy_ps), "bottom",  (0, 1))
+stokes.add_dirichlet_bc((vx_ps, vy_ps), "left", (0, 1))
+stokes.add_dirichlet_bc((vx_ps, vy_ps), "right", (0, 1))
+
+
+
+
+
+
+
# linear solve first
+
+stokes.solve()
+
+
+
+
+
+
+
# Now introduce the non-linearity once we have an initial strain rate
+
+mu = 0.25
+tau_y = sympy.Max(3.5 + mu * stokes.p.sym[0], 0.1)
+viscosity = sympy.Min(tau_y / (2 * stokes.Unknowns.Einv2 + 0.01), 1.0)
+# viscosity = 100 * (0.01 + stokes._Einv2)
+stokes.constitutive_model.Parameters.viscosity = viscosity
+stokes.saddle_preconditioner = 1 / viscosity
+
+
+
+
+
+
+
# Approach the required value by shifting the parameters
+
+for i in range(1): #5
+    mu = 0.25
+    C = 2.5 + (1 - i / 4) * 1.0
+    print(f"Mu - {mu}, C = {C}")
+    tau_y = sympy.Max(C + mu * stokes.p.sym[0], 0.1)
+    viscosity = sympy.Min(tau_y / (2 * stokes.Unknowns.Einv2 + 0.01), 1.0)
+    # viscosity = 100 * (0.01 + stokes._Einv2)
+    stokes.constitutive_model.Parameters.viscosity = viscosity
+    stokes.saddle_preconditioner = 1 / viscosity
+    stokes.solve(zero_init_guess=False)
+
+
+
+
+
+
+
nodal_tau_inv2.uw_function = (
+    stokes.constitutive_model.Parameters.viscosity * stokes.Unknowns.Einv2
+)
+nodal_tau_inv2.solve()
+nodal_visc_calc.uw_function = stokes.constitutive_model.Parameters.viscosity
+nodal_visc_calc.solve()
+nodal_strain_rate_inv2.solve()
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+
+if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+    
+    pvmesh = vis.mesh_to_pv_mesh(mesh1)
+    pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym)
+    pvmesh.point_data["Edot"] = vis.scalar_fn_to_pv_points(pvmesh, strain_rate_inv2.sym)
+    pvmesh.point_data["Visc"] = vis.scalar_fn_to_pv_points(pvmesh, node_viscosity.sym)
+    pvmesh.point_data["Str"] = vis.scalar_fn_to_pv_points(pvmesh, dev_stress_inv2.sym)
+    pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)
+    pvmesh.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh, v_soln.sym.dot(v_soln.sym))
+
+    velocity_points = vis.meshVariable_to_pv_cloud(v_soln)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym)
+
+    # point sources at cell centres
+    points = np.zeros((mesh1._centroids.shape[0], 3))
+    points[:, 0] = mesh1._centroids[:, 0]
+    points[:, 1] = mesh1._centroids[:, 1]
+    point_cloud = pv.PolyData(points)
+
+    pvstream = pvmesh.streamlines_from_source(
+        point_cloud, vectors="V", integration_direction="both", max_steps=100
+    )
+
+    pl = pv.Plotter(window_size=(1000, 500))
+
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.1, opacity=0.75)
+
+    # pl.add_points(point_cloud, cmap="coolwarm",
+    #               render_points_as_spheres=False,
+    #               point_size=10, opacity=0.66
+    #             )
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        scalars="Edot",
+        use_transparency=False,
+        opacity=1.0,
+    )  # clim=[0.0,1.0])
+
+    # pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.75)
+    # pl.add_mesh(pvstream)
+
+    # pl.remove_scalar_bar("mag")
+
+    pl.show()
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Sandbox-VEP/Ex_Shear_Band_Plasticity_SS.html b/main/Notebooks/Examples-Sandbox-VEP/Ex_Shear_Band_Plasticity_SS.html new file mode 100644 index 0000000..0b67032 --- /dev/null +++ b/main/Notebooks/Examples-Sandbox-VEP/Ex_Shear_Band_Plasticity_SS.html @@ -0,0 +1,915 @@ + + + + + + + + + + + Shear bands around a circular inclusion in a simple shear flow — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Shear bands around a circular inclusion in a simple shear flow

+ +
+
+ +
+
+
+ + + + +
+ +
+

Shear bands around a circular inclusion in a simple shear flow#

+

No slip conditions

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
expt_name = "ShearBand"
+
+
+
+
+
+
+
import petsc4py
+import underworld3 as uw
+import numpy as np
+
+
+
+
+
+
+
import meshio, pygmsh
+
+# Mesh a 2D pipe with a circular hole
+
+csize = 0.075
+csize_circle = 0.025
+res = csize_circle
+
+width = 3.0
+height = 1.0
+radius = 0.1
+
+if uw.mpi.rank == 0:
+    # Generate local mesh on boss process
+
+    with pygmsh.geo.Geometry() as geom:
+        geom.characteristic_length_max = csize
+
+        inclusion = geom.add_circle(
+            (0.0, 0.0, 0.0), radius, make_surface=False, mesh_size=csize_circle
+        )
+        domain = geom.add_rectangle(
+            xmin=-width / 2,
+            ymin=-height / 2,
+            xmax=width / 2,
+            ymax=height / 2,
+            z=0,
+            holes=[inclusion],
+            mesh_size=csize,
+        )
+
+        geom.add_physical(domain.surface.curve_loop.curves[0], label="bottom")
+        geom.add_physical(domain.surface.curve_loop.curves[1], label="right")
+        geom.add_physical(domain.surface.curve_loop.curves[2], label="top")
+        geom.add_physical(domain.surface.curve_loop.curves[3], label="left")
+
+        geom.add_physical(inclusion.curve_loop.curves, label="inclusion")
+
+        geom.add_physical(domain.surface, label="Elements")
+
+        geom.generate_mesh(dim=2, verbose=False)
+        geom.save_geometry("tmp_shear_inclusion.msh")
+
+
+
+
+
+
+
mesh1 = uw.discretisation.Mesh("tmp_shear_inclusion.msh", simplex=True)
+
+
+
+
+
+
+
# Define some functions on the mesh
+
+import sympy
+
+# radius_fn = sympy.sqrt(mesh1.rvec.dot(mesh1.rvec)) # normalise by outer radius if not 1.0
+# unit_rvec = mesh1.rvec / (1.0e-10+radius_fn)
+
+# Some useful coordinate stuff
+
+x, y = mesh1.X
+
+# relative to the centre of the inclusion
+r = sympy.sqrt(x**2 + y**2)
+th = sympy.atan2(y, x)
+
+# need a unit_r_vec equivalent
+
+inclusion_rvec = mesh1.X
+inclusion_unit_rvec = inclusion_rvec / inclusion_rvec.dot(inclusion_rvec)
+inclusion_unit_rvec = mesh1.vector.to_matrix(inclusion_unit_rvec)
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable("U", mesh1, mesh1.dim, degree=2)
+p_soln = uw.discretisation.MeshVariable("P", mesh1, 1, degree=1, continuous=False)
+p_cont = uw.discretisation.MeshVariable("Pc", mesh1, 1, degree=1, continuous=True)
+p_null = uw.discretisation.MeshVariable(r"P2", mesh1, 1, degree=1, continuous=True)
+
+vorticity = uw.discretisation.MeshVariable("omega", mesh1, 1, degree=1)
+strain_rate_inv2 = uw.discretisation.MeshVariable("eps", mesh1, 1, degree=1)
+dev_stress_inv2 = uw.discretisation.MeshVariable("tau", mesh1, 1, degree=1)
+node_viscosity = uw.discretisation.MeshVariable("eta", mesh1, 1, degree=1)
+r_inc = uw.discretisation.MeshVariable("R", mesh1, 1, degree=1)
+
+
+
+
+
+
+
# Create NS object
+
+stokes = uw.systems.Stokes(
+    mesh1,
+    velocityField=v_soln,
+    pressureField=p_soln,
+    verbose=False,
+    solver_name="stokes",
+)
+
+
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.viscosity = 1
+stokes.saddle_preconditioner = 1 / stokes.constitutive_model.Parameters.viscosity
+stokes.penalty = 0.0
+
+stokes.petsc_options["ksp_monitor"] = None
+stokes.petsc_options["snes_atol"] = 0.001
+
+
+
+
+
+
+
nodal_strain_rate_inv2 = uw.systems.Projection(
+    mesh1, strain_rate_inv2, solver_name="edot_II"
+)
+nodal_strain_rate_inv2.add_dirichlet_bc(1.0, "top", 0)
+nodal_strain_rate_inv2.add_dirichlet_bc(1.0, "bottom", 0)
+nodal_strain_rate_inv2.uw_function = stokes.Unknowns.Einv2
+nodal_strain_rate_inv2.smoothing = 1.0e-3
+nodal_strain_rate_inv2.petsc_options.delValue("ksp_monitor")
+
+nodal_tau_inv2 = uw.systems.Projection(mesh1, dev_stress_inv2, solver_name="stress_II")
+S = stokes.stress_deviator
+nodal_tau_inv2.uw_function = (
+    sympy.simplify(sympy.sqrt(((S**2).trace()) / 2)) - p_soln.sym[0]
+)
+nodal_tau_inv2.smoothing = 1.0e-3
+nodal_tau_inv2.petsc_options.delValue("ksp_monitor")
+
+nodal_visc_calc = uw.systems.Projection(mesh1, node_viscosity, solver_name="visc")
+nodal_visc_calc.uw_function = stokes.constitutive_model.Parameters.viscosity
+nodal_visc_calc.smoothing = 1.0e-3
+nodal_visc_calc.petsc_options.delValue("ksp_monitor")
+
+# nodal_pres_calc = uw.systems.Projection(mesh1, p_cont, solver_name="pres")
+# nodal_pres_calc.uw_function = p_soln.sym[0]
+# nodal_pres_calc.smoothing = 1.0e-3
+# nodal_pres_calc.petsc_options.delValue("ksp_monitor")
+
+
+
+
+
+
+
# Set solve options here (or remove default values
+# stokes.petsc_options.getAll()
+
+# Constant visc
+
+stokes.penalty = 0.0
+stokes.bodyforce = 1.0e-32 * mesh1.CoordinateSystem.unit_e_1
+
+hw = 1000.0 / res
+surface_defn_fn = sympy.exp(-(((r - radius) / radius) ** 2) * hw)
+# stokes.bodyforce -= 1.0e6 * surface_defn_fn * v_soln.sym.dot(inclusion_unit_rvec) * inclusion_unit_rvec
+
+# Velocity boundary conditions
+
+stokes.add_dirichlet_bc((0.0, 0.0), "inclusion", (0, 1))
+stokes.add_dirichlet_bc((1.0, 0.0), "top", (0, 1))
+stokes.add_dirichlet_bc((-1.0, 0.0), "bottom", (0, 1))
+stokes.add_dirichlet_bc(0.0, "left", 1)
+stokes.add_dirichlet_bc(0.0, "right", 1)
+
+
+
+
+
+
+
# linear solve first
+
+stokes.solve()
+
+
+
+
+
+
+
# Approach the required non-linear value by gradually adjusting the parameters
+
+steps = 8
+for i in range(steps):
+    mu = 0.5
+    C = 2.5 + (steps - i) * 0.33
+    print(f"Mu - {mu}, C = {C}")
+    tau_y = sympy.Max(C + mu * stokes.p.sym[0], 0.1)
+    viscosity = sympy.Min(tau_y / (2 * stokes.Unknowns.Einv2 + 0.01), 1.0)
+    # viscosity = 100 * (0.01 + stokes._Einv2)
+    stokes.constitutive_model.Parameters.viscosity = viscosity
+    stokes.saddle_preconditioner = 1 / viscosity
+    stokes.solve(zero_init_guess=False)
+
+
+
+
+
+
+
nodal_tau_inv2.uw_function = (
+    stokes.constitutive_model.Parameters.viscosity * stokes.Unknowns.Einv2
+)
+nodal_tau_inv2.solve()
+nodal_visc_calc.uw_function = stokes.constitutive_model.Parameters.viscosity
+nodal_visc_calc.solve()
+nodal_strain_rate_inv2.solve()
+# nodal_pres_calc.solve()
+
+
+
+
+
+
+
with mesh1.access():
+    print(v_soln.data)
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+
+if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh1)
+    pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_cont.sym)
+    pvmesh.point_data["Edot"] = vis.scalar_fn_to_pv_points(pvmesh, strain_rate_inv2.sym)
+    pvmesh.point_data["Visc"] = vis.scalar_fn_to_pv_points(pvmesh, node_viscosity.sym)
+    pvmesh.point_data["Str"] = vis.scalar_fn_to_pv_points(pvmesh, dev_stress_inv2.sym)
+    pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)
+    pvmesh.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh, v_soln.sym.dot(v_soln.sym))
+
+    velocity_points = vis.meshVariable_to_pv_cloud(v_soln)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym)
+
+
+    # point sources at cell centres
+
+    subsample = 10
+    points = np.zeros((mesh1._centroids[::subsample].shape[0], 3))
+    points[:, 0] = mesh1._centroids[::subsample, 0]
+    points[:, 1] = mesh1._centroids[::subsample, 1]
+    point_cloud = pv.PolyData(points)
+
+    pvstream = pvmesh.streamlines_from_source(
+        point_cloud, vectors="V", integration_direction="both", max_steps=100
+    )
+
+    pl = pv.Plotter(window_size=(1000, 500))
+
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.1, opacity=0.75)
+    pl.camera_position = "xy"
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Grey",
+        show_edges=True,
+        clim=[1.0, 2.0],
+        scalars="Edot",
+        use_transparency=False,
+        opacity=1.0,
+    )
+
+    # pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.75)
+    pl.add_mesh(pvstream)
+
+    # pl.remove_scalar_bar("mag")
+
+    pl.show()
+
+
+
+
+
+
+
pvmesh.point_data["Visc"].min(), pvmesh.point_data["Visc"].max()
+
+
+
+
+
+
+
pvmesh.point_data["P"].min(), pvmesh.point_data["P"].max()  # cf 4.26
+
+
+
+
+
+
+
pvmesh.point_data["Str"].min(), pvmesh.point_data["Str"].max()
+
+
+
+
+
+
+
pvmesh.point_data["Edot"].min(), pvmesh.point_data["Edot"].max()
+
+
+
+
+
+
+
pvmesh.point_data["V"].min(), pvmesh.point_data["V"].max()
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Sandbox-VEP/Ex_Sheared_Layer_Elastic.html b/main/Notebooks/Examples-Sandbox-VEP/Ex_Sheared_Layer_Elastic.html new file mode 100644 index 0000000..1f41a85 --- /dev/null +++ b/main/Notebooks/Examples-Sandbox-VEP/Ex_Sheared_Layer_Elastic.html @@ -0,0 +1,1515 @@ + + + + + + + + + + + Validate constitutive models — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Validate constitutive models

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

Validate constitutive models#

+

Simple shear with material defined by particle swarm (based on inclusion model), position, pressure, strain rate etc. Check the implementation of the Jacobians using various non-linear terms.

+

Check elastic stress terms

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import os
+
+os.environ["UW_TIMING_ENABLE"] = "1"
+
+import petsc4py
+import underworld3 as uw
+import numpy as np
+import sympy
+import pyvista as pv
+import vtk
+
+from underworld3 import timing
+
+resolution = uw.options.getReal("model_resolution", default=0.05)
+mu = uw.options.getInt("mu", default=0.5)
+maxsteps = uw.options.getInt("max_steps", default=500)
+
+
+## Define units here and physical timestep numbers etc.
+
+observation_timescale = 0.01
+
+
+
+
+
+
+
# Mesh a 2D pipe with a circular hole
+
+mesh1 = uw.meshing.UnstructuredSimplexBox(
+    minCoords=(-1.5, -0.5),
+    maxCoords=(+1.5, +0.5),
+    cellSize=resolution,
+)
+
+
+
+
+
+
+

+mesh1.dm.view()
+
+## build periodic mesh (mesh1)
+# uw.cython.petsc_discretisation.petsc_dm_set_periodicity(
+#     mesh1.dm, [0.1, 0.0], [-1.5, 0.0], [1.5, 0.0])
+
+# mesh1.dm.view()
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable("U", mesh1, mesh1.dim, degree=2)
+p_soln = uw.discretisation.MeshVariable(
+    "P", mesh1, 1, vtype=uw.VarType.SCALAR, degree=1, continuous=True
+)
+Stress = uw.discretisation.MeshVariable(
+    r"Stress",
+    mesh1,
+    (2, 2),
+    vtype=uw.VarType.SYM_TENSOR,
+    degree=2,
+    continuous=True,
+    varsymbol=r"{\sigma}",
+)
+work = uw.discretisation.MeshVariable(
+    "W", mesh1, 1, vtype=uw.VarType.SCALAR, degree=2, continuous=True
+)
+strain_rate_inv2 = uw.discretisation.MeshVariable(
+    "eps_dot", mesh1, 1, degree=2, varsymbol=r"{\dot\varepsilon}"
+)
+strain_rate_inv2_pl = uw.discretisation.MeshVariable(
+    "eps_dot_pl", mesh1, 1, degree=2, varsymbol=r"{\dot\varepsilon_{pl}}"
+)
+dev_stress_inv2 = uw.discretisation.MeshVariable("tau", mesh1, 1, degree=2)
+
+
+
+
+
+
+
mesh1.view()
+
+
+
+
+
+
+
swarm = uw.swarm.Swarm(mesh=mesh1, recycle_rate=5)
+
+material = uw.swarm.SwarmVariable(
+    "M",
+    swarm,
+    size=1,
+    vtype=uw.VarType.SCALAR,
+    proxy_continuous=True,
+    proxy_degree=1,
+    dtype=int,
+)
+
+strain = uw.swarm.SwarmVariable(
+    "Strain",
+    swarm,
+    size=1,
+    vtype=uw.VarType.SCALAR,
+    proxy_continuous=True,
+    proxy_degree=2,
+    varsymbol=r"\varepsilon",
+    dtype=float,
+)
+
+stress_star_p = uw.swarm.SwarmVariable(
+    r"stress_p",
+    swarm,
+    (2, 2),
+    vtype=uw.VarType.SYM_TENSOR,
+    proxy_continuous=True,
+    proxy_degree=2,
+    varsymbol=r"{\sigma^{*}_{p}}",
+)
+
+swarm.populate(fill_param=2)
+
+stress_star_update_dt = uw.swarm.Lagrangian_Updater(
+    swarm, Stress.sym, [stress_star_p], dt_physical=observation_timescale
+)
+
+
+
+
+
+
+
# Some useful coordinate stuff
+x, y = mesh1.X
+
+
+
+
+
+
+
with swarm.access(strain, material), mesh1.access():
+    XX = swarm.particle_coordinates.data[:, 0]
+    YY = swarm.particle_coordinates.data[:, 1]
+    mask = (1.0 - (YY * 2) ** 8) * (1 - (2 * XX / 3) ** 6)
+    material.data[(XX**2 + YY**2 < 0.01), 0] = 1
+    strain.data[:, 0] = (
+        0.01 * np.random.random(swarm.particle_coordinates.data.shape[0]) * mask
+    )
+
+
+
+
+
+
+
# Create Solver object
+
+stokes = uw.systems.Stokes(
+    mesh1,
+    velocityField=v_soln,
+    pressureField=p_soln,
+    verbose=False,
+    solver_name="stokes",
+)
+
+viscosity_L = sympy.Piecewise(
+    (1, material.sym[0] > 0.5),
+    (1000, True),
+)
+
+
+
+
+
+
+
stokes.constitutive_model = uw.constitutive_models.ViscoElasticPlasticFlowModel(
+    u=v_soln, flux_dt=stress_star_update_dt
+)
+
+
+
+
+
+
+
stokes.constitutive_model.Parameters.shear_viscosity_0 = viscosity_L
+stokes.constitutive_model.Parameters.shear_modulus = sympy.sympify(100)
+stokes.constitutive_model.Parameters.stress_star = stress_star_p.sym
+stokes.constitutive_model.Parameters.dt_elastic = sympy.sympify(observation_timescale)
+
+
+
+
+
+
+
stokes.constitutive_model
+
+
+
+
+
+
+
sigma_projector = uw.systems.Tensor_Projection(
+    mesh1, tensor_Field=Stress, scalar_Field=work
+)
+sigma_projector.uw_function = stokes.stress_1d
+
+
+
+
+
+
+
nodal_strain_rate_inv2 = uw.systems.Projection(
+    mesh1, strain_rate_inv2, solver_name="edot_II"
+)
+
+nodal_strain_rate_inv2.uw_function = stokes._Einv2
+nodal_strain_rate_inv2.smoothing = 1.0e-3
+
+nodal_tau_inv2 = uw.systems.Projection(mesh1, dev_stress_inv2, solver_name="stress_II")
+nodal_tau_inv2.uw_function = 2 * stokes.constitutive_model.viscosity * stokes._Einv2
+nodal_tau_inv2.smoothing = 1.0e-3
+
+
+
+
+
+
+
# Set solve options here (or remove default values
+# stokes.petsc_options.getAll()
+
+# Constant visc
+
+stokes.penalty = 1.0
+stokes.tolerance = 1.0e-4
+
+# Velocity boundary conditions
+
+stokes.add_dirichlet_bc((0.0, 0.0), "Inclusion", (0, 1))
+stokes.add_dirichlet_bc((1.0, 0.0), "Top", (0, 1))
+stokes.add_dirichlet_bc((-1.0, 0.0), "Bottom", (0, 1))
+stokes.add_dirichlet_bc((0.0), "Left", (1))
+stokes.add_dirichlet_bc((0.0), "Right", (1))
+
+
+
+
+
+
+
stress_star_update_dt.psi_star[0].sym
+
+
+
+
+
+
+
stokes.solve()
+
+
+
+
+
+
+
stokes.constitutive_model.Parameters.strainrate_inv_II_min = 0.00001
+stokes.constitutive_model.Parameters.yield_stress = 50
+
+
+
+
+
+
+
stokes
+
+
+
+
+
+
+
stokes.stress[0, 0]
+
+
+
+
+
+
+
nodal_strain_rate_inv2.solve()
+
+sigma_projector.uw_function = stokes.stress_deviator
+sigma_projector.solve()
+
+
+
+
+
+
+
with swarm.access(stress_star_p), mesh1.access():
+    stress_star_p.data[
+        ...
+    ] = 0.0  # Stress.rbf_interpolate(swarm.particle_coordinates.data)
+
+
+
+
+
+
+
timing.reset()
+timing.start()
+
+
+
+
+
+
+
print("Setup terms", flush=True)
+
+
+
+
+
+
+
stokes._setup_terms()
+
+
+
+
+
+
+
stokes.stress[0, 0]
+
+
+
+
+
+
+
stokes.solve(zero_init_guess=False, verbose=True)
+timing.print_table(display_fraction=1)
+print(stokes._u.max(), stokes._p.max())
+
+
+
+
+
+
+
nodal_strain_rate_inv2.uw_function = stokes._Einv2
+nodal_strain_rate_inv2.solve()
+
+S = stokes.stress_deviator
+nodal_tau_inv2.uw_function = stokes.constitutive_model.viscosity * 2 * stokes._Einv2
+nodal_tau_inv2.solve()
+
+
+
+
+
+
+
stokes.constitutive_model.flux_dt
+
+
+
+
+
+
+
# check it - NOTE - for the periodic mesh, points which have crossed the coordinate sheet are plotted somewhere
+# unexpected. This is a limitation we are stuck with for the moment.
+
+if uw.mpi.size == 1:
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh1)
+
+    pvpoints = pvmesh.points[:, 0:2]
+    usol = v_soln.rbf_interpolate(pvpoints)
+
+    pvmesh.point_data["P"] = p_soln.rbf_interpolate(pvpoints)
+    pvmesh.point_data["Edot"] = strain_rate_inv2.rbf_interpolate(pvpoints)
+    pvmesh.point_data["Strs"] = dev_stress_inv2.rbf_interpolate(pvpoints)
+    pvmesh.point_data["Mat"] = material.rbf_interpolate(pvpoints)
+    pvmesh.point_data["Strn"] = strain._meshVar.rbf_interpolate(pvpoints)
+    pvmesh.point_data["SStar"] = stress_star_p._meshVar.rbf_interpolate(pvpoints)
+
+    # Velocity arrows
+
+    v_vectors = np.zeros_like(pvmesh.points)
+    v_vectors[:, 0:2] = v_soln.rbf_interpolate(pvpoints)
+
+    # Points (swarm)
+
+    with swarm.access():
+        points = np.zeros((swarm.data.shape[0], 3))
+        points[:, 0] = swarm.data[:, 0]
+        points[:, 1] = swarm.data[:, 1]
+        point_cloud = pv.PolyData(points)
+        point_cloud.point_data["strain"] = strain.data[:, 0]
+
+    pl = pv.Plotter(window_size=(500, 500))
+
+    pl.add_arrows(pvmesh.points, v_vectors, mag=0.1, opacity=0.75)
+    # pl.camera_position = "xy"
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="Blues",
+        edge_color="Grey",
+        show_edges=True,
+        # clim=[0.0,1.0],
+        scalars="Strs",
+        use_transparency=False,
+        opacity=0.5,
+    )
+
+    # pl.add_points(point_cloud, colormap="coolwarm", scalars="strain", point_size=10.0, opacity=0.5)
+
+    pl.camera.SetPosition(0.0, 0.0, 3.0)
+    pl.camera.SetFocalPoint(0.0, 0.0, 0.0)
+    pl.camera.SetClippingRange(1.0, 8.0)
+
+    pl.show()
+
+
+
+
+
+
+
def return_points_to_domain(coords):
+    new_coords = coords.copy()
+    new_coords[:, 0] = (coords[:, 0] + 1.5) % 3 - 1.5
+    return new_coords
+
+
+
+
+
+
+
ts = 0
+
+
+
+
+
+
+
stress_star_update_dt.view()
+
+
+
+
+
+
+
expt_name = f"shear_band_sw_nonp_{mu}"
+
+for step in range(0, 75):
+    stokes.solve(zero_init_guess=False)
+
+    delta_t = 0.01
+
+    nodal_strain_rate_inv2.uw_function = sympy.Max(
+        0.0,
+        stokes._Einv2
+        - 0.5
+        * stokes.constitutive_model.Parameters.yield_stress
+        / stokes.constitutive_model.Parameters.shear_viscosity_0,
+    )
+    nodal_strain_rate_inv2.solve()
+
+    with mesh1.access(strain_rate_inv2_pl):
+        strain_rate_inv2_pl.data[...] = strain_rate_inv2.data.copy()
+
+    nodal_strain_rate_inv2.uw_function = stokes._Einv2
+    nodal_strain_rate_inv2.solve()
+
+    S = stokes.stress_deviator
+    nodal_tau_inv2.uw_function = sympy.simplify(sympy.sqrt(((S**2).trace()) / 2))
+    nodal_tau_inv2.solve()
+
+    if uw.mpi.rank == 0:
+        print(f"Stress Inv II -  {dev_stress_inv2.mean()}")
+
+    sigma_projector.solve()
+    stress_star_update_dt.update(dt=delta_t, evalf=True)
+
+    with swarm.access(strain), mesh1.access():
+        XX = swarm.particle_coordinates.data[:, 0]
+        YY = swarm.particle_coordinates.data[:, 1]
+        mask = (2 * XX / 3) ** 4  # * 1.0 - (YY * 2)**8
+        strain.data[:, 0] += (
+            delta_t * mask * strain_rate_inv2_pl.rbf_interpolate(swarm.data)[:, 0]
+            - 0.1 * delta_t
+        )
+        strain_dat = (
+            delta_t * mask * strain_rate_inv2_pl.rbf_interpolate(swarm.data)[:, 0]
+        )
+
+        if uw.mpi.rank == 0:
+            print(f"Sstar[0,0]     = {(np.sqrt(stress_star_p[0,0].data[:]**2)).mean()}")
+            print(f"Sstar[1,0]     = {(np.sqrt(stress_star_p[0,1].data[:]**2)).mean()}")
+            print(f"Sstar[1,1]     = {(np.sqrt(stress_star_p[1,1].data[:]**2)).mean()}")
+
+    mesh1.write_timestep(
+        expt_name,
+        meshUpdates=False,
+        meshVars=[p_soln, v_soln, strain_rate_inv2_pl],
+        outputPath="output",
+        index=ts,
+    )
+
+    swarm.save(f"{expt_name}.swarm.{ts}.h5")
+    strain.save(f"{expt_name}.strain.{ts}.h5")
+
+    # Update the swarm locations
+    swarm.advection(
+        v_soln.sym, delta_t=delta_t, restore_points_to_domain_func=None, evalf=True
+    )
+
+    if uw.mpi.rank == 0:
+        print("Timestep {}, dt {}".format(step, delta_t))
+
+    ts += 1
+
+
+
+
+
+
+
stokes.constitutive_model.stress_projection()[0, 0]
+
+
+
+
+
+
+
stokes.stress[0, 0]
+
+
+
+
+
+
+
# check it - NOTE - for the periodic mesh, points which have crossed the coordinate sheet are plotted somewhere
+# unexpected. This is a limitation we are stuck with for the moment.
+
+if uw.mpi.size == 1:
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh1)
+
+    pvpoints = pvmesh.points[:, 0:2]
+    usol = v_soln.rbf_interpolate(pvpoints)
+
+    pvmesh.point_data["P"] = p_soln.rbf_interpolate(pvpoints)
+    pvmesh.point_data["Edot"] = strain_rate_inv2.rbf_interpolate(pvpoints)
+    pvmesh.point_data["Strs"] = dev_stress_inv2.rbf_interpolate(pvpoints)
+    pvmesh.point_data["Mat"] = material.rbf_interpolate(pvpoints)
+    pvmesh.point_data["Strn"] = strain._meshVar.rbf_interpolate(pvpoints)
+    pvmesh.point_data["SStar"] = uw.function.evalf(stress_star_p[1, 1].sym, pvpoints)
+
+    # Velocity arrows
+
+    v_vectors = np.zeros_like(pvmesh.points)
+    v_vectors[:, 0:2] = v_soln.rbf_interpolate(pvpoints)
+
+    # Points (swarm)
+
+    with swarm.access():
+        points = np.zeros((swarm.data.shape[0], 3))
+        points[:, 0] = swarm.data[:, 0]
+        points[:, 1] = swarm.data[:, 1]
+        point_cloud = pv.PolyData(points)
+        point_cloud.point_data["strain"] = strain.data[:, 0]
+
+    pl = pv.Plotter(window_size=(500, 500))
+
+    pl.add_arrows(pvmesh.points, v_vectors, mag=0.1, opacity=0.75)
+    # pl.camera_position = "xy"
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="Blues",
+        edge_color="Grey",
+        show_edges=True,
+        # clim=[0.0,1.0],
+        scalars="Mat",
+        use_transparency=False,
+        opacity=0.5,
+    )
+
+    # pl.add_points(point_cloud, colormap="coolwarm", scalars="strain", point_size=10.0, opacity=0.5)
+
+    pl.camera.SetPosition(0.0, 0.0, 3.0)
+    pl.camera.SetFocalPoint(0.0, 0.0, 0.0)
+    pl.camera.SetClippingRange(1.0, 8.0)
+
+    pl.show()
+
+
+
+
+
+
+
stress_star_p._meshVar.min()
+
+
+
+
+
+
+
nodal_tau_inv2.snes.cancelMonitor()
+
+
+
+
+
+
+
0 / 0
+
+
+
+
+
+
+
# Adams / Bashforth & Adams Moulton ...
+
+s = sympy.Symbol(r"\sigma")
+s1 = sympy.Symbol(r"\sigma^*")
+s2 = sympy.Symbol(r"\sigma^**")
+dt = sympy.Symbol(r"\Delta t")
+mu = sympy.Symbol(r"\mu")
+eta = sympy.Symbol(r"\eta")
+edot = sympy.Symbol(r"\dot\varepsilon")
+tr = sympy.Symbol(r"t_r")
+
+sdot1 = (s - s1) / dt
+sdot2 = (3 * s - 4 * s1 + s2) / (2 * dt)
+
+
+
+
+
+
+
display(sdot1)
+display(sdot2)
+Seq1 = sympy.Equality(sympy.simplify(sdot1 / (2 * mu) + s / (2 * eta)), edot)
+display(Seq1)
+sympy.simplify(sympy.solve(Seq1, s)[0])
+
+
+
+
+
+
+
eta_eff_1 = sympy.simplify(eta * mu * dt / (mu * dt + eta))
+display(eta_eff_1)
+a = sympy.simplify(2 * eta * sympy.solve(Seq1, s)[0] / (2 * eta_eff_1))
+tau_1 = a.subs(eta / mu, tr)
+tau_1
+
+
+
+
+
+
+
Seq2 = sympy.Equality(sympy.simplify(sdot2 / (2 * mu) + s / (2 * eta)), edot)
+display(Seq2)
+sympy.simplify(sympy.solve(Seq2, s)[0])
+
+
+
+
+
+
+
eta_eff_2 = sympy.simplify(2 * eta * mu * dt / (2 * mu * dt + 3 * eta))
+display(eta_eff_2)
+sympy.simplify(2 * eta * sympy.solve(Seq2, s)[0] / (2 * eta_eff_2))
+
+
+
+
+
+
+
a = sympy.simplify(2 * eta * sympy.solve(Seq2, s)[0] / (2 * eta_eff_2))
+tau_2 = a.expand().subs(eta / mu, tr)
+
+
+
+
+
+
+
tau_2
+
+
+
+
+
+
+
# 0/0
+
+
+
+
+
+
+
stokes.constitutive_model.Parameters.yield_stress.subs(
+    ((strain.sym[0], 0.25), (y, 0.0))
+)
+
+
+
+
+
+
+
stokes.constitutive_model.Parameters.viscosity
+
+
+
+
+
+
+
def return_points_to_domain(coords):
+    new_coords = coords.copy()
+    new_coords[:, 0] = (coords[:, 0] + 1.5) % 3 - 1.5
+    return new_coords
+
+
+
+
+
+
+
ts = 0
+
+
+
+
+
+
+
expt_name = f"output/shear_band_sw_nonp_{mu}"
+
+for step in range(0, 10):
+    stokes.solve(zero_init_guess=False)
+
+    delta_t = stokes.estimate_dt()
+
+    nodal_strain_rate_inv2.uw_function = sympy.Max(
+        0.0,
+        stokes._Einv2
+        - 0.5
+        * stokes.constitutive_model.Parameters.yield_stress
+        / stokes.constitutive_model.Parameters.shear_viscosity_0,
+    )
+    nodal_strain_rate_inv2.solve()
+
+    with mesh1.access(strain_rate_inv2_pl):
+        strain_rate_inv2_pl.data[...] = strain_rate_inv2.data.copy()
+
+    nodal_strain_rate_inv2.uw_function = stokes._Einv2
+    nodal_strain_rate_inv2.solve()
+
+    with swarm.access(strain), mesh1.access():
+        XX = swarm.particle_coordinates.data[:, 0]
+        YY = swarm.particle_coordinates.data[:, 1]
+        mask = (2 * XX / 3) ** 4  # * 1.0 - (YY * 2)**8
+        strain.data[:, 0] += (
+            delta_t * mask * strain_rate_inv2_pl.rbf_interpolate(swarm.data)[:, 0]
+            - 0.1 * delta_t
+        )
+        strain_dat = (
+            delta_t * mask * strain_rate_inv2_pl.rbf_interpolate(swarm.data)[:, 0]
+        )
+        print(
+            f"dStrain / dt = {delta_t * (mask * strain_rate_inv2_pl.rbf_interpolate(swarm.data)[:,0]).mean()}, {delta_t}"
+        )
+
+    mesh1.write_timestep_xdmf(
+        f"{expt_name}",
+        meshUpdates=False,
+        meshVars=[p_soln, v_soln, strain_rate_inv2_pl],
+        swarmVars=[strain],
+        index=ts,
+    )
+
+    swarm.save(f"{expt_name}.swarm.{ts}.h5")
+    strain.save(f"{expt_name}.strain.{ts}.h5")
+
+    # Update the swarm locations
+    swarm.advection(v_soln.sym, delta_t=delta_t, restore_points_to_domain_func=None)
+
+    if uw.mpi.rank == 0:
+        print("Timestep {}, dt {}".format(step, delta_t))
+
+    ts += 1
+
+
+
+
+
+
+
# nodal_visc_calc.uw_function = sympy.log(stokes.constitutive_model.Parameters.viscosity)
+# nodal_visc_calc.solve()
+
+# yield_stress_calc.uw_function = stokes.constitutive_model.Parameters.yield_stress
+# yield_stress_calc.solve()
+
+nodal_tau_inv2.uw_function = (
+    2 * stokes.constitutive_model.Parameters.viscosity * stokes._Einv2
+)
+nodal_tau_inv2.solve()
+
+
+
+
+
+
+
# check it - NOTE - for the periodic mesh, points which have crossed the coordinate sheet are plotted somewhere
+# unexpected. This is a limitation we are stuck with for the moment.
+
+if uw.mpi.size == 1:
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh1)
+
+    pvpoints = pvmesh.points[:, 0:2]
+    usol = v_soln.rbf_interpolate(pvpoints)
+
+    pvmesh.point_data["P"] = p_soln.rbf_interpolate(pvpoints)
+    pvmesh.point_data["Edot"] = strain_rate_inv2.rbf_interpolate(pvpoints)
+    # pvmesh.point_data["Visc"] = np.exp(node_viscosity.rbf_interpolate(pvpoints))
+    pvmesh.point_data["Edotp"] = strain_rate_inv2_pl.rbf_interpolate(pvpoints)
+    pvmesh.point_data["Strs"] = dev_stress_inv2.rbf_interpolate(pvpoints)
+    # pvmesh.point_data["StrY"] =  yield_stress.rbf_interpolate(pvpoints)
+    # pvmesh.point_data["dStrY"] = pvmesh.point_data["StrY"] - 2 *  pvmesh.point_data["Visc"] * pvmesh.point_data["Edot"]
+    pvmesh.point_data["Mat"] = material.rbf_interpolate(pvpoints)
+    pvmesh.point_data["Strn"] = strain._meshVar.rbf_interpolate(pvpoints)
+
+    # Velocity arrows
+
+    v_vectors = np.zeros_like(pvmesh.points)
+    v_vectors[:, 0:2] = v_soln.rbf_interpolate(pvpoints)
+
+    # Points (swarm)
+
+    with swarm.access():
+        plot_points = np.where(strain.data > 0.0001)
+        strain_data = strain.data.copy()
+
+        points = np.zeros((swarm.data[plot_points].shape[0], 3))
+        points[:, 0] = swarm.data[plot_points[0], 0]
+        points[:, 1] = swarm.data[plot_points[0], 1]
+        point_cloud = pv.PolyData(points)
+        point_cloud.point_data["strain"] = strain.data[plot_points]
+
+    pl = pv.Plotter(window_size=(500, 500))
+
+    # pl.add_arrows(pvmesh.points, v_vectors, mag=0.1, opacity=0.75)
+    # pl.camera_position = "xy"
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="Blues",
+        edge_color="Grey",
+        show_edges=True,
+        # clim=[-1.0,1.0],
+        scalars="Edotp",
+        use_transparency=False,
+        opacity=0.5,
+    )
+
+    pl.add_points(
+        point_cloud,
+        colormap="Oranges",
+        scalars="strain",
+        point_size=10.0,
+        opacity=0.0,
+        # clim=[0.0,0.2],
+    )
+
+    pl.camera.SetPosition(0.0, 0.0, 3.0)
+    pl.camera.SetFocalPoint(0.0, 0.0, 0.0)
+    pl.camera.SetClippingRange(1.0, 8.0)
+
+    pl.show()
+
+
+
+
+
+
+
pvmesh.point_data["Strn"].shape
+
+
+
+
+
+
+
import matplotlib.pyplot as plt
+
+
+
+
+
+
+
plt.scatter(mesh1.data[:, 0], mesh1.data[:, 1], c=pvmesh.point_data["Strn"])
+
+
+
+
+
+
+
with swarm.access():
+    print(strain.data.max())
+
+
+
+
+
+
+
strain_rate_inv2_pl.rbf_interpolate(mesh1.data).max()
+
+
+
+
+
+

#

+
+
+
mesh1._search_lengths
+
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Sandbox-VEP/Ex_Sheared_Layer_Test.html b/main/Notebooks/Examples-Sandbox-VEP/Ex_Sheared_Layer_Test.html new file mode 100644 index 0000000..c5db420 --- /dev/null +++ b/main/Notebooks/Examples-Sandbox-VEP/Ex_Sheared_Layer_Test.html @@ -0,0 +1,1411 @@ + + + + + + + + + + + Validate constitutive models — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Validate constitutive models

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

Validate constitutive models#

+

Simple shear with material defined by particle swarm (based on inclusion model), position, pressure, strain rate etc. +Check the implementation of the Jacobians using various non-linear terms.

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import os
+os.environ["UW_TIMING_ENABLE"] = "1"
+
+import petsc4py
+import underworld3 as uw
+import numpy as np
+import sympy
+
+import pyvista as pv
+import vtk
+
+from underworld3 import timing
+
+resolution = uw.options.getReal("model_resolution", default=0.033)
+mu = uw.options.getInt("mu", default=0.5)
+maxsteps = uw.options.getInt("max_steps", default=500)
+
+
+
+
+
+
+
+
# Mesh a 2D pipe with a circular hole
+
+csize = resolution
+csize_circle = resolution * 0.5
+res = csize
+cellSize = csize
+
+width = 3.0
+height = 1.0
+radius = 0.0
+
+eta1 = 1000
+eta2 = 1
+
+from enum import Enum
+
+## NOTE: stop using pygmsh, then we can just define boundary labels ourselves and not second guess pygmsh
+
+class boundaries(Enum):
+    Bottom = 1
+    Right = 3
+    Top = 2
+    Left  = 4
+    Inclusion = 5
+    All_Boundaries = 1001 
+
+
+
+if uw.mpi.rank == 0:
+
+    import gmsh
+
+    gmsh.initialize()
+    gmsh.model.add("Periodic x")
+
+    xmin, ymin = -width / 2, -height / 2
+    xmax, ymax = +width / 2, +height / 2
+
+    p1 = gmsh.model.geo.add_point(xmin, ymin, 0.0, meshSize=cellSize)
+    p2 = gmsh.model.geo.add_point(xmax, ymin, 0.0, meshSize=cellSize)
+    p3 = gmsh.model.geo.add_point(xmin, ymax, 0.0, meshSize=cellSize)
+    p4 = gmsh.model.geo.add_point(xmax, ymax, 0.0, meshSize=cellSize)
+
+    l1 = gmsh.model.geo.add_line(p1, p2, tag=boundaries["Bottom"].value)
+    l2 = gmsh.model.geo.add_line(p2, p4, tag=boundaries["Right"].value)
+    l3 = gmsh.model.geo.add_line(p4, p3, tag=boundaries["Top"].value)
+    l4 = gmsh.model.geo.add_line(p3, p1, tag=boundaries["Left"].value)
+
+    loops = []
+    if radius > 0.0:
+        p5 = gmsh.model.geo.add_point(0.0, 0.0, 0.0, meshSize=csize_circle)
+        p6 = gmsh.model.geo.add_point(+radius, 0.0, 0.0, meshSize=csize_circle)
+        p7 = gmsh.model.geo.add_point(-radius, 0.0, 0.0, meshSize=csize_circle)
+
+        c1 = gmsh.model.geo.add_circle_arc(p6, p5, p7)
+        c2 = gmsh.model.geo.add_circle_arc(p7, p5, p6)
+
+        cl1 = gmsh.model.geo.add_curve_loop([c1, c2], tag=55)
+        loops = [cl1] + loops
+
+    cl = gmsh.model.geo.add_curve_loop((l1, l2, l3, l4))
+    loops = [cl] + loops
+
+    surface = gmsh.model.geo.add_plane_surface(loops, tag=99999)
+
+    gmsh.model.geo.synchronize()
+
+    # translation = [1, 0, 0, width, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
+    # gmsh.model.mesh.setPeriodic(
+    #     1, [boundaries["Right"]], [boundaries["Left"]], translation
+    # )
+
+    # Add Physical groups
+
+    for bd in boundaries:
+        print(bd.value, flush=True)
+        print(bd.name, flush=True)
+        gmsh.model.add_physical_group(1, [bd.value], bd.value)
+        gmsh.model.set_physical_name(1, bd.value, bd.name)
+
+    if radius > 0.0:
+        gmsh.model.addPhysicalGroup(1, [c1, c2], 55)
+        gmsh.model.setPhysicalName(1, 55, "Inclusion")
+
+    gmsh.model.addPhysicalGroup(2, [surface], surface)
+    gmsh.model.setPhysicalName(2, surface, "Elements")
+
+    # %%
+    gmsh.model.mesh.generate(2)
+    gmsh.write("tmp_shear_inclusion.msh")
+    gmsh.finalize()
+
+
+
+
+
+
+
mesh1 = uw.discretisation.Mesh("tmp_shear_inclusion.msh", 
+                               simplex=True, markVertices=True, 
+                               useRegions=True, boundaries=boundaries
+                              )
+
+## build periodic mesh (mesh1)
+# mesh1.view()
+# uw.cython.petsc_discretisation.petsc_dm_set_periodicity(
+#     mesh1.dm, [0.1, 0.0], [-1.5, 0.0], [1.5, 0.0])
+
+
+
+
+
+
+

+swarm = uw.swarm.Swarm(mesh=mesh1, recycle_rate=5)
+
+material = uw.swarm.SwarmVariable(
+    "M", swarm, size=1, 
+    proxy_continuous=True, proxy_degree=2, dtype=int,
+)
+
+strain_p = uw.swarm.SwarmVariable(
+    "Strain_p", swarm, size=1, 
+    proxy_continuous=True, 
+    proxy_degree=2, varsymbol=r"{\varepsilon_{p}}", dtype=float,
+)
+
+stress_dt = uw.swarm.SwarmVariable(r"Stress_p", swarm, (2,2), vtype=uw.VarType.SYM_TENSOR, varsymbol=r"{\sigma^{*}_{p}}")
+
+swarm.populate(fill_param=2)
+
+
+
+
+
+
+
# Define some functions on the mesh
+
+import sympy
+
+# Some useful coordinate stuff
+
+x, y = mesh1.X
+
+# relative to the centre of the inclusion
+r = sympy.sqrt(x**2 + y**2)
+th = sympy.atan2(y, x)
+
+# need a unit_r_vec equivalent
+
+inclusion_rvec = mesh1.X
+inclusion_unit_rvec = inclusion_rvec / inclusion_rvec.dot(inclusion_rvec)
+inclusion_unit_rvec = mesh1.vector.to_matrix(inclusion_unit_rvec)
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable("U", mesh1, mesh1.dim, degree=2)
+p_soln = uw.discretisation.MeshVariable("P", mesh1, 1, degree=1, continuous=True)
+work   = uw.discretisation.MeshVariable(r"W", mesh1, 1, degree=1, continuous=False)
+Stress = uw.discretisation.MeshVariable(r"{\sigma}", mesh1, (2,2), vtype=uw.VarType.SYM_TENSOR, degree=1, 
+                                         continuous=False, varsymbol=r"{\sigma}")
+
+vorticity = uw.discretisation.MeshVariable("omega", mesh1, 1, degree=1)
+strain_rate_inv2 = uw.discretisation.MeshVariable("eps", mesh1, 1, degree=2)
+strain_rate_inv2_p = uw.discretisation.MeshVariable("eps_p", mesh1, 1, degree=2, varsymbol=r"\dot\varepsilon_p")
+dev_stress_inv2 = uw.discretisation.MeshVariable("tau", mesh1, 1, degree=2)
+yield_stress = uw.discretisation.MeshVariable("tau_y", mesh1, 1, degree=1)
+
+node_viscosity = uw.discretisation.MeshVariable("eta", mesh1, 1, degree=1)
+r_inc = uw.discretisation.MeshVariable("R", mesh1, 1, degree=1)
+
+
+
+
+
+
+
mesh1.view()
+
+
+
+
+
+
+
0/0
+
+
+
+
+
+
+
# Set the initial strain from the mesh 
+
+with mesh1.access(): #strain_rate_inv2_p):
+    XX = v_soln.coords[:,0]
+    YY = v_soln.coords[:,1]
+    mask = (1.0 - (YY * 2)**8) * (1 -  (2*XX/3)**6)
+    # strain_rate_inv2_p.data[:,0] = 2.0 * np.floor(0.033+np.random.random(strain_rate_inv2_p.coords.shape[0])) * mask
+
+
+
+
+
+
+
   
+with swarm.access(material, strain_p), mesh1.access():
+    strain_p.data[:] = strain_rate_inv2_p.rbf_interpolate(swarm.particle_coordinates.data)
+    strain_array = strain_rate_inv2_p.rbf_interpolate(swarm.particle_coordinates.data)
+
+
+
+
+
+
+
   
+with swarm.access(strain_p, material), mesh1.access():
+    XX = swarm.particle_coordinates.data[:,0]
+    YY = swarm.particle_coordinates.data[:,1]
+    mask = (1.0 - (YY * 2)**8) * (1 -  (2*XX/3)**6)
+    material.data[(XX**2 + YY**2 < 0.01), 0] = 1
+    strain_p.data[:,0] = 0.0 * np.random.random(swarm.particle_coordinates.data.shape[0]) * mask
+
+
+
+
+
+
+
# Create Solver object
+
+stokes = uw.systems.Stokes(
+    mesh1,
+    velocityField=v_soln,
+    pressureField=p_soln,
+    verbose=True,
+    solver_name="stokes",
+)
+
+eta1 = 1000
+eta2 = 1
+
+viscosity_L = sympy.Piecewise(
+    (eta2, material.sym[0] > 0.5),
+    (eta1, True),
+)
+
+
+
+
+
+
+
stokes.constitutive_model = uw.constitutive_models.ViscoElasticPlasticFlowModel
+stokes.constitutive_model.Parameters.bg_viscosity = viscosity_L
+# stokes.constitutive_model.Parameters.sigma_star_fn 
+
+
+
+
+
+
+
stokes.constitutive_model.viscosity
+
+
+
+
+
+
+
stokes.constitutive_model
+
+
+
+
+
+
+
stokes.constitutive_model.Parameters.shear_modulus = 1.0
+stokes.constitutive_model.Parameters.dt_elastic = 0.1
+
+
+
+
+
+
+
stokes.constitutive_model
+
+
+
+
+
+
+
stokes.stress_deviator_1d
+
+
+
+
+
+
+
sigma_projector = uw.systems.Tensor_Projection(mesh1, tensor_Field=Stress, scalar_Field=work  )
+sigma_projector.uw_function = stokes.stress_1d
+
+
+
+
+
+
+
nodal_strain_rate_inv2 = uw.systems.Projection(
+    mesh1, strain_rate_inv2, solver_name="edot_II"
+)
+nodal_strain_rate_inv2.uw_function = stokes.Unknowns.Einv2
+nodal_strain_rate_inv2.smoothing = 1.0e-3
+nodal_strain_rate_inv2.petsc_options.delValue("ksp_monitor")
+
+nodal_tau_inv2 = uw.systems.Projection(mesh1, dev_stress_inv2, solver_name="stress_II")
+nodal_tau_inv2.uw_function = (
+    2 * stokes.constitutive_model.viscosity * stokes.Unknowns.Einv2
+)
+nodal_tau_inv2.smoothing = 1.0e-3
+nodal_tau_inv2.petsc_options.delValue("ksp_monitor")
+
+yield_stress_calc = uw.systems.Projection(mesh1, yield_stress, solver_name="stress_y")
+yield_stress_calc.uw_function = 0.0
+yield_stress_calc.smoothing = 1.0e-3
+yield_stress_calc.petsc_options.delValue("ksp_monitor")
+
+nodal_visc_calc = uw.systems.Projection(mesh1, node_viscosity, solver_name="visc")
+nodal_visc_calc.uw_function = stokes.constitutive_model.viscosity
+nodal_visc_calc.smoothing = 1.0e-3
+nodal_visc_calc.petsc_options.delValue("ksp_monitor")
+
+
+
+
+
+
+
# Set solve options here (or remove default values
+# stokes.petsc_options.getAll()
+
+# Constant visc
+
+stokes.penalty = 1.0
+stokes.bodyforce = (
+    -0.00000001 * mesh1.CoordinateSystem.unit_e_1.T 
+)  # vertical force term (non-zero pressure)
+
+stokes.tolerance = 1.0e-4
+
+# stokes.bodyforce -= 1.0e6 * surface_defn_fn * v_soln.sym.dot(inclusion_unit_rvec) * inclusion_unit_rvec
+
+# Velocity boundary conditions
+
+if radius > 0.0:
+    stokes.add_dirichlet_bc((0.0, 0.0), "Inclusion")
+    
+stokes.add_dirichlet_bc((1.0, 0.0), "Top")
+stokes.add_dirichlet_bc((-1.0, 0.0), "Bottom")
+stokes.add_dirichlet_bc((sympy.oo, 0.0), "Left")
+stokes.add_dirichlet_bc((sympy.oo, 0.0), "Right")
+
+
+
+
+
+
+
mesh1.dm.view()
+
+
+
+
+
+
+
stokes._setup_pointwise_functions()
+stokes._setup_discretisation(verbose=True)
+stokes._setup_solver()
+
+
+
+
+
+
+
stokes.solve()
+
+
+
+
+
+
+
sigma_projector.solve()
+
+
+
+
+
+
+
with mesh1.access():
+    print(Stress[0,0].data.max())
+    print(Stress[1,1].data.max())
+    print(Stress[0,1].data.max())
+
+
+
+
+
+
+
# Now add yield without pressure dependence
+
+eps_ref = sympy.sympify(1)
+scale = sympy.sympify(25)
+C0 = 2500
+Cinf = 500
+
+# C = 2 * (y * 2)**16 + 5.0 * sympy.exp(-(strain.sym[0]/0.1)**2) + 0.1
+C = 2 * (y * 2)**2 + (C0-Cinf) * (1 - sympy.tanh((strain_p.sym[0]/eps_ref - 1)*scale) ) / 2 + Cinf
+
+stokes.constitutive_model.Parameters.yield_stress = C + mu * p_soln.sym[0]
+stokes.constitutive_model.Parameters.edot_II_fn = stokes.Unknowns.Einv2
+stokes.constitutive_model.Parameters.min_viscosity = 0.1
+stokes.saddle_preconditioner = 1 / stokes.constitutive_model.viscosity
+
+
+# stokes.solve(zero_init_guess=False, picard=2)
+
+stokes.constitutive_model
+
+
+
+
+
+
+
stokes.constitutive_model.stress_projection()
+
+
+
+
+
+
+
stokes.constitutive_model.flux
+
+
+
+
+
+
+
0/0
+
+
+
+
+
+
+
with mesh1.access():
+    print(p_soln.data.min(), p_soln.data.max())
+
+
+
+
+
+
+
timing.reset()
+timing.start()
+
+stokes.snes.setType("newtontr")
+stokes.solve(zero_init_guess=False, picard = -1)
+
+timing.print_table(display_fraction=1)
+
+
+
+
+
+
+
stokes._u_f1
+
+
+
+
+
+
+

+S = stokes.stress_deviator
+nodal_tau_inv2.uw_function = sympy.simplify(sympy.sqrt(((S**2).trace()) / 2))
+nodal_tau_inv2.solve()
+
+yield_stress_calc.uw_function = stokes.constitutive_model.Parameters.yield_stress
+yield_stress_calc.solve()
+
+nodal_visc_calc.uw_function = sympy.log(stokes.constitutive_model.Parameters.viscosity)
+nodal_visc_calc.solve()
+
+nodal_strain_rate_inv2.uw_function = (sympy.Max(0.0, stokes._Einv2 - 
+                        0.5 * stokes.constitutive_model.Parameters.yield_stress / stokes.constitutive_model.Parameters.bg_viscosity))
+nodal_strain_rate_inv2.solve()
+
+with mesh1.access(strain_rate_inv2_p):
+    strain_rate_inv2_p.data[...] = strain_rate_inv2.data.copy()
+
+nodal_strain_rate_inv2.uw_function = stokes._Einv2
+nodal_strain_rate_inv2.solve()
+
+
+
+
+
+
+
mesh0 = uw.meshing.UnstructuredSimplexBox(
+    minCoords=(-1.5,-0.5),
+    maxCoords=(+1.5,+0.5),
+    cellSize=0.033,
+)
+
+
+
+
+
+
+
# check it - NOTE - for the periodic mesh, points which have crossed the coordinate sheet are plotted somewhere
+# unexpected. This is a limitation we are stuck with for the moment.
+
+if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh1)
+
+    pvpoints = pvmesh.points[:, 0:2]
+    usol = v_soln.rbf_interpolate(pvpoints)
+
+    pvmesh.point_data["P"] = p_soln.rbf_interpolate(pvpoints)
+    pvmesh.point_data["Edot"] = strain_rate_inv2.rbf_interpolate(pvpoints)**2
+    pvmesh.point_data["Edotp"] = strain_rate_inv2_p.rbf_interpolate(pvpoints)
+    pvmesh.point_data["Strs"] = dev_stress_inv2.rbf_interpolate(pvpoints)
+    pvmesh.point_data["StrY"] =  yield_stress.rbf_interpolate(pvpoints)
+    pvmesh.point_data["dStrY"] = pvmesh.point_data["Strs"] - pvmesh.point_data["StrY"]
+    pvmesh.point_data["Visc"] = np.exp(node_viscosity.rbf_interpolate(pvpoints))
+    pvmesh.point_data["Mat"] = material.rbf_interpolate(pvpoints)
+    pvmesh.point_data["Strn"] = strain._meshVar.rbf_interpolate(pvpoints)
+
+    # Velocity arrows
+    
+    v_vectors = np.zeros_like(pvmesh.points)
+    v_vectors[:, 0:2] = v_soln.rbf_interpolate(pvpoints)
+        
+    # Points (swarm)
+    
+    with swarm.access():
+        points = np.zeros((swarm.data.shape[0], 3))
+        points[:, 0] = swarm.data[:,0]
+        points[:, 1] = swarm.data[:,1]
+        point_cloud = pv.PolyData(points)
+        point_cloud.point_data["strain"] = strain.data[:,0]
+
+    pl = pv.Plotter(window_size=(500, 500))
+
+    # pl.add_arrows(pvmesh.points, v_vectors, mag=0.1, opacity=0.75)
+    # pl.camera_position = "xy"
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="Blues",
+        edge_color="Grey",
+        show_edges=True,
+        # clim=[0.0,1.0],
+        scalars="P",
+        use_transparency=False,
+        opacity=0.5,
+    )
+    
+    # pl.add_points(point_cloud, colormap="coolwarm", scalars="strain", point_size=10.0, opacity=0.5)
+    
+    pl.camera.SetPosition(0.0, 0.0, 3.0)
+    pl.camera.SetFocalPoint(0.0, 0.0, 0.0)
+    pl.camera.SetClippingRange(1.0, 8.0)
+        
+    pl.show()
+
+
+
+
+
+
+
# Now add elasticity
+
+
+
+
+
+
+
stokes.constitutive_model.Parameters.yield_stress.subs(((strain.sym[0],0.25), (y,0.0)))
+
+
+
+
+
+
+
stokes.constitutive_model.Parameters.viscosity
+
+
+
+
+
+
+
def return_points_to_domain(coords):
+    new_coords = coords.copy()
+    new_coords[:,0] = (coords[:,0] + 1.5)%3 - 1.5
+    return new_coords
+
+
+
+
+
+
+
ts = 0
+
+
+
+
+
+
+
delta_t = stokes.estimate_dt()
+
+expt_name = f"output/shear_band_sw_nonp_{mu}"
+
+for step in range(0, 10):
+    
+    stokes.solve(zero_init_guess=False)
+    
+    nodal_strain_rate_inv2.uw_function = (sympy.Max(0.0, stokes._Einv2 - 
+                       0.5 * stokes.constitutive_model.Parameters.yield_stress / stokes.constitutive_model.Parameters.bg_viscosity))
+    nodal_strain_rate_inv2.solve()
+
+    with mesh1.access(strain_rate_inv2_p):
+        strain_rate_inv2_p.data[...] = strain_rate_inv2.data.copy()
+        
+    nodal_strain_rate_inv2.uw_function = stokes._Einv2
+    nodal_strain_rate_inv2.solve()
+    
+    with swarm.access(strain), mesh1.access():
+        XX = swarm.particle_coordinates.data[:,0]
+        YY = swarm.particle_coordinates.data[:,1]
+        mask =  (2*XX/3)**4 # * 1.0 - (YY * 2)**8 
+        strain.data[:,0] +=  delta_t * mask * strain_rate_inv2_p.rbf_interpolate(swarm.data)[:,0] - 0.1 * delta_t
+        strain_dat = delta_t * mask *  strain_rate_inv2_p.rbf_interpolate(swarm.data)[:,0]
+        print(f"dStrain / dt = {delta_t * (mask * strain_rate_inv2_p.rbf_interpolate(swarm.data)[:,0]).mean()}, {delta_t}")
+        
+    mesh1.write_timestep_xdmf(f"{expt_name}", 
+                         meshUpdates=False,
+                         meshVars=[p_soln,v_soln,strain_rate_inv2_p], 
+                         swarmVars=[strain],
+                         index=ts)
+    
+    swarm.save(f"{expt_name}.swarm.{ts}.h5")
+    strain.save(f"{expt_name}.strain.{ts}.h5")
+
+    # Update the swarm locations
+    swarm.advection(v_soln.sym, delta_t=delta_t, 
+                 restore_points_to_domain_func=None) 
+    
+    if uw.mpi.rank == 0:
+        print("Timestep {}, dt {}".format(step, delta_t))
+        
+    ts += 1
+
+
+
+
+
+
+
nodal_visc_calc.uw_function = sympy.log(stokes.constitutive_model.Parameters.viscosity)
+nodal_visc_calc.solve()
+
+yield_stress_calc.uw_function = stokes.constitutive_model.Parameters.yield_stress
+yield_stress_calc.solve()
+
+nodal_tau_inv2.uw_function = 2 * stokes.constitutive_model.Parameters.viscosity * stokes._Einv2
+nodal_tau_inv2.solve()
+
+
+
+
+
+
+
# check it - NOTE - for the periodic mesh, points which have crossed the coordinate sheet are plotted somewhere
+# unexpected. This is a limitation we are stuck with for the moment.
+
+if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh1)
+
+    pvpoints = pvmesh.points[:, 0:2]
+    usol = v_soln.rbf_interpolate(pvpoints)
+
+    pvmesh.point_data["P"] = p_soln.rbf_interpolate(pvpoints)
+    pvmesh.point_data["Edot"] = strain_rate_inv2.rbf_interpolate(pvpoints)
+    pvmesh.point_data["Visc"] = np.exp(node_viscosity.rbf_interpolate(pvpoints))
+    pvmesh.point_data["Edotp"] = strain_rate_inv2_p.rbf_interpolate(pvpoints)
+    pvmesh.point_data["Strs"] = dev_stress_inv2.rbf_interpolate(pvpoints)
+    pvmesh.point_data["StrY"] =  yield_stress.rbf_interpolate(pvpoints)
+    pvmesh.point_data["dStrY"] = pvmesh.point_data["StrY"] - 2 *  pvmesh.point_data["Visc"] * pvmesh.point_data["Edot"] 
+    pvmesh.point_data["Mat"] = material.rbf_interpolate(pvpoints)
+    pvmesh.point_data["Strn"] = strain._meshVar.rbf_interpolate(pvpoints)
+
+    # Velocity arrows
+    
+    v_vectors = np.zeros_like(pvmesh.points)
+    v_vectors[:, 0:2] = v_soln.rbf_interpolate(pvpoints)
+        
+    # Points (swarm)
+    
+    with swarm.access():
+        plot_points = np.where(strain.data > 0.0001)
+        strain_data = strain.data.copy()
+
+        points = np.zeros((swarm.data[plot_points].shape[0], 3))
+        points[:, 0] = swarm.data[plot_points[0],0]
+        points[:, 1] = swarm.data[plot_points[0],1]
+        point_cloud = pv.PolyData(points)
+        point_cloud.point_data["strain"] = strain.data[plot_points]
+
+    pl = pv.Plotter(window_size=(500, 500))
+
+    # pl.add_arrows(pvmesh.points, v_vectors, mag=0.1, opacity=0.75)
+    # pl.camera_position = "xy"
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="Blues",
+        edge_color="Grey",
+        show_edges=True,
+        # clim=[-1.0,1.0],
+        scalars="Edotp",
+        use_transparency=False,
+        opacity=0.5,
+    )
+    
+ 
+    pl.add_points(point_cloud, 
+                  colormap="Oranges", scalars="strain",
+                  point_size=10.0, 
+                  opacity=0.0,
+                  # clim=[0.0,0.2],
+                 )
+    
+    pl.camera.SetPosition(0.0, 0.0, 3.0)
+    pl.camera.SetFocalPoint(0.0, 0.0, 0.0)
+    pl.camera.SetClippingRange(1.0, 8.0)
+        
+    pl.show()
+
+
+
+
+
+
+
strain_dat.max()
+
+
+
+
+
+
+
with swarm.access():
+    print(strain.data.max())
+
+
+
+
+
+
+
strain_rate_inv2_p.rbf_interpolate(mesh1.data).max()
+
+
+
+
+
+

#

+
+
+
mesh1._search_lengths
+
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Sandbox-VEP/Ex_Sheared_Layer_Visualisation.html b/main/Notebooks/Examples-Sandbox-VEP/Ex_Sheared_Layer_Visualisation.html new file mode 100644 index 0000000..c1a8208 --- /dev/null +++ b/main/Notebooks/Examples-Sandbox-VEP/Ex_Sheared_Layer_Visualisation.html @@ -0,0 +1,728 @@ + + + + + + + + + + + Validate constitutive models — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Validate constitutive models

+ +
+
+ +
+
+
+ + + + +
+ +
+

Validate constitutive models#

+

Simple shear with material defined by particle swarm (based on inclusion model), position, pressure, strain rate etc. Check the implmentation of the Jacobians using various non-linear terms.

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+import underworld3 as uw
+import numpy as np
+
+import pyvista as pv
+import vtk
+
+pv.global_theme.background = "white"
+pv.global_theme.window_size = [1250, 500]
+pv.global_theme.anti_aliasing = "ssaa"
+pv.global_theme.jupyter_backend = "trame"
+pv.global_theme.smooth_shading = True
+pv.global_theme.camera["viewup"] = [0.0, 1.0, 0.0]
+pv.global_theme.camera["position"] = [0.0, 0.0, 20.0]
+
+
+
+
+
+
+
step = 1 #50
+
+# basename = "/Users/lmoresi/+Simulations/ShearTest/ShearTestHP_InclusionMu05/shear_band_sw_nonp_0.5"
+basename = "output/shear_band_sw_nonp_0.5"
+
+
+
+
+
+
+
%%sh
+ls -trl "/Users/lmoresi/+Simulations/ShearTest/ShearTestHP_InclusionMu05/" | tail
+
+
+
+
+
+
+
# ls /Users/lmoresi/+Simulations/ShearTest/ShearMu05_res0.01
+
+
+
+
+
+
+
# Simplified (not periodic) mesh that covers the 
+# same area as the original (periodic) mesh
+
+mesh1 = uw.meshing.UnstructuredSimplexBox(
+    minCoords=(-1.5,-0.5),
+    maxCoords=(+1.5,+0.5),
+    cellSize=0.02,
+)
+
+swarm = uw.swarm.Swarm(mesh=mesh1)
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable("U", mesh1, mesh1.dim, degree=2)
+p_soln = uw.discretisation.MeshVariable("P", mesh1, 1, degree=1, continuous=True)
+
+strain_rate_inv2 = uw.discretisation.MeshVariable("eps", mesh1, 1, degree=1)
+strain_rate_inv2_p = uw.discretisation.MeshVariable("eps_p", mesh1, 1, degree=1, varsymbol=r"\dot\varepsilon_p")
+strain_p = uw.discretisation.MeshVariable("p_strain", mesh1, 1, degree=2, varsymbol=r"varepsilon_p")
+
+strain = uw.swarm.SwarmVariable(
+    "Strain", swarm, size=1, 
+    proxy_degree=1, proxy_continuous=False, 
+    varsymbol=r"\varepsilon", dtype=float,
+)
+
+# dev_stress_inv2 = uw.discretisation.MeshVariable("tau", mesh1, 1, degree=2)
+# yield_stress = uw.discretisation.MeshVariable("tau_y", mesh1, 1, degree=1)
+
+swarm.load(f"{basename}.swarm.{step}.h5")
+strain.load(f"{basename}.strain.{step}.h5", swarmFilename=f"{basename}.swarm.{step}.h5")
+
+
+
+
+
+
+
v_soln.read_from_vertex_checkpoint(f"{basename}.U.{step}.h5", "U")
+p_soln.read_from_vertex_checkpoint(f"{basename}.P.{step}.h5", "P")
+strain_rate_inv2_p.read_from_vertex_checkpoint(f"{basename}.eps_p.{step}.h5", "eps_p")
+strain_p.read_from_vertex_checkpoint(f"{basename}.proxy.Strain.{step}.h5", "proxy_Strain")
+
+
+
+
+
+
+
mesh1.vtk("tmp_shear_inclusion.vtk")
+pvmesh = pv.read("tmp_shear_inclusion.vtk")
+
+pvpoints = pvmesh.points[:, 0:2]
+usol = v_soln.rbf_interpolate(pvpoints)
+
+
+pvmesh.point_data["P"] = p_soln.rbf_interpolate(pvpoints)
+pvmesh.point_data["Edotp"] = strain_rate_inv2_p.rbf_interpolate(pvpoints)
+pvmesh.point_data["Strn"] = strain_p.rbf_interpolate(pvpoints)
+pvmesh.point_data["Strn2"] = strain._meshVar.rbf_interpolate(pvpoints)
+
+# Velocity arrows
+
+v_vectors = np.zeros_like(pvmesh.points)
+v_vectors[:, 0:2] = v_soln.rbf_interpolate(pvpoints)
+
+# Points (swarm)
+
+with swarm.access():
+    plot_points = np.where(strain.data > 0.0)
+    strain_data = strain.data.copy()
+    
+    points = np.zeros((swarm.data[plot_points].shape[0], 3))
+    points[:, 0] = swarm.data[plot_points[0],0]
+    points[:, 1] = swarm.data[plot_points[0],1]
+    point_cloud = pv.PolyData(points)
+    point_cloud.point_data["strain"] = strain.data[plot_points]
+
+pl = pv.Plotter(window_size=(500, 500))
+
+# pl.add_arrows(pvmesh.points, v_vectors, mag=0.05, opacity=0.25)
+# pl.camera_position = "xy"
+
+
+pl.add_mesh(
+    pvmesh,
+    cmap="Blues",
+    edge_color="Grey",
+    show_edges=False,
+    # clim=[0.1,0.5],
+    scalars="Edotp",
+    use_transparency=False,
+    opacity=0.75,
+)
+
+pl.add_points(point_cloud, colormap="Oranges", 
+              scalars="strain", 
+              # clim=[0.0,0.001],
+              point_size=5.0,
+              opacity=0.5)
+
+pl.camera.SetPosition(0.0, 0.0, 3.0)
+pl.camera.SetFocalPoint(0.0, 0.0, 0.0)
+pl.camera.SetClippingRange(1.0, 8.0)
+
+# pl.camera.SetPosition(0.75, 0.2, 1.5)
+# pl.camera.SetFocalPoint(0.75, 0.2, 0.0)
+
+
+pl.screenshot(
+            filename=f"{basename}.{step}.png",
+            window_size=(2560, 1280),
+            return_img=False,
+        )
+    
+
+pl.show()
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Sandbox-VEP/Ex_VP-CM_slabSubduction.html b/main/Notebooks/Examples-Sandbox-VEP/Ex_VP-CM_slabSubduction.html new file mode 100644 index 0000000..1ca0008 --- /dev/null +++ b/main/Notebooks/Examples-Sandbox-VEP/Ex_VP-CM_slabSubduction.html @@ -0,0 +1,1150 @@ + + + + + + + + + + + Slab subduction — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + +
+ +
+

Slab subduction#

+
+

From Dan Sandiford#

+

UW2 example ported to UW3

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import numpy as np
+import os
+import math
+from petsc4py import PETSc
+import underworld3 as uw
+
+
+from underworld3.utilities import generateXdmf
+
+
+from sympy import Piecewise, ceiling, Abs, Min, sqrt, eye, Matrix, Max
+
+
+
+
+
+
+
expt_name = "output/slabSubduction/"
+
+if uw.mpi.rank == 0:
+    ### delete previous model run
+    if os.path.exists(expt_name):
+        for i in os.listdir(expt_name):
+            os.remove(expt_name + i)
+
+    ### create folder if not run before
+    if not os.path.exists(expt_name):
+        os.makedirs(expt_name)
+
+
+
+
+
+
+
### For visualisation
+render = True
+
+
+
+
+
+
+

+
+options = PETSc.Options()
+
+options["snes_converged_reason"] = None
+options["snes_monitor_short"] = None
+
+
+
+
+
+
+
n_els = 30
+dim = 2
+boxLength = 4.0
+boxHeight = 1.0
+ppcell = 5
+
+
+
+
+
+
+

Create mesh and mesh vars#

+
+
+
mesh = uw.meshing.StructuredQuadBox(
+    elementRes=(4 * n_els, n_els),
+    minCoords=(0.0,) * dim,
+    maxCoords=(boxLength, boxHeight),
+)
+
+
+v = uw.discretisation.MeshVariable("V", mesh, mesh.dim, degree=2)
+p = uw.discretisation.MeshVariable("P", mesh, 1, degree=1)
+
+strain_rate_inv2 = uw.discretisation.MeshVariable("SR", mesh, 1, degree=1)
+node_viscosity = uw.discretisation.MeshVariable("Viscosity", mesh, 1, degree=1)
+# materialField    = uw.discretisation.MeshVariable("Material", mesh, 1, degree=1)
+
+stokes = uw.systems.Stokes(mesh, velocityField=v, pressureField=p)
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+
+
+
+
+
+
+

Create swarm and swarm vars#

+
    +
  • ‘swarm.add_variable’ is a traditional swarm, can’t be used to map material properties. Can be used for sympy operations, similar to mesh vars.

  • +
  • ‘uw.swarm.IndexSwarmVariable’, creates a mask for each material and can be used to map material properties. Can’t be used for sympy operations.

  • +
+
+
+
swarm = uw.swarm.Swarm(mesh)
+
+
+
+
+
+
+
## # Add index swarm variable for material
+material = uw.swarm.IndexSwarmVariable("M", swarm, indices=5)
+
+
+
+
+
+
+

+
+swarm.populate(3)
+
+# Add some randomness to the particle distribution
+import numpy as np
+
+np.random.seed(0)
+
+with swarm.access(swarm.particle_coordinates):
+    factor = 0.5 * boxLength / n_els / ppcell
+    swarm.particle_coordinates.data[:] += factor * np.random.rand(
+        *swarm.particle_coordinates.data.shape
+    )
+
+
+
+
+
+

Project fields to mesh vars#

+

Useful for visualising stuff on the mesh (Viscosity, material, strain rate etc) and saving to a grouped xdmf file

+
+
+
# material.info()
+# """
+# you have 5 materials
+# if you want to have material variable rheologies, density
+# """
+# phi 1 = material.piecewise([m1_visc,2,3,4,5])
+# phi_2 = material.piecewise([m1_rho, m2_rho, m3_rho])
+
+
+
+
+
+
+
stokes.constitutive_model.Parameters.shear_viscosity_0
+
+
+
+
+
+
+
nodal_strain_rate_inv2 = uw.systems.Projection(mesh, strain_rate_inv2)
+nodal_strain_rate_inv2.uw_function = stokes.Unknowns.Einv2
+# nodal_strain_rate_inv2.smoothing = 1.0e-3
+nodal_strain_rate_inv2.petsc_options.delValue("ksp_monitor")
+
+nodal_visc_calc = uw.systems.Projection(mesh, node_viscosity)
+nodal_visc_calc.uw_function = stokes.constitutive_model.Parameters.shear_viscosity_0
+# nodal_visc_calc.smoothing = 1.0e-3
+nodal_visc_calc.petsc_options.delValue("ksp_monitor")
+
+# meshMat = uw.systems.Projection(mesh, materialField)
+# meshMat.uw_function = material.sym
+# # meshMat.smoothing = 1.0e-3
+# meshMat.petsc_options.delValue("ksp_monitor")
+
+
+def updateFields():
+    ### update strain rate
+    nodal_strain_rate_inv2.uw_function = stokes.Unknowns.Einv2
+    nodal_strain_rate_inv2.solve()
+
+    ### update viscosity
+    nodal_visc_calc.uw_function = stokes.constitutive_model.Parameters.shear_viscosity_0
+    nodal_visc_calc.solve(_force_setup=True)
+
+    # ### update material field from swarm
+    # meshMat.uw_function = material.sym
+    # meshMat.solve(_force_setup=True)
+
+
+
+
+
+
+
+

Setup the material distribution#

+
+
+
import matplotlib.path as mpltPath
+
+### initialise the 'material' data to represent two different materials.
+upperMantleIndex = 0
+lowerMantleIndex = 1
+upperSlabIndex = 2
+lowerSlabIndex = 3
+coreSlabIndex = 4
+
+### Initial material layout has a flat lying slab with at 15\degree perturbation
+lowerMantleY = 0.4
+slabLowerShape = np.array(
+    [
+        (1.2, 0.925),
+        (3.25, 0.925),
+        (3.20, 0.900),
+        (1.2, 0.900),
+        (1.02, 0.825),
+        (1.02, 0.850),
+    ]
+)
+slabCoreShape = np.array(
+    [
+        (1.2, 0.975),
+        (3.35, 0.975),
+        (3.25, 0.925),
+        (1.2, 0.925),
+        (1.02, 0.850),
+        (1.02, 0.900),
+    ]
+)
+slabUpperShape = np.array(
+    [
+        (1.2, 1.000),
+        (3.40, 1.000),
+        (3.35, 0.975),
+        (1.2, 0.975),
+        (1.02, 0.900),
+        (1.02, 0.925),
+    ]
+)
+
+
+
+
+
+
+
slabLower = mpltPath.Path(slabLowerShape)
+slabCore = mpltPath.Path(slabCoreShape)
+slabUpper = mpltPath.Path(slabUpperShape)
+
+
+
+
+
+

Update the material variable of the swarm#

+
+
+
with swarm.access(swarm.particle_coordinates, material):
+    ### for the symbolic mapping of material properties
+    material.data[:] = upperMantleIndex
+    material.data[
+        swarm.particle_coordinates.data[:, 1] < lowerMantleY
+    ] = lowerMantleIndex
+    material.data[
+        slabLower.contains_points(swarm.particle_coordinates.data[:])
+    ] = lowerSlabIndex
+    material.data[
+        slabCore.contains_points(swarm.particle_coordinates.data[:])
+    ] = coreSlabIndex
+    material.data[
+        slabUpper.contains_points(swarm.particle_coordinates.data[:])
+    ] = upperSlabIndex
+
+
+
+
+
+
+
def plot_mat():
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh)
+
+    points = vis.swarm_to_pv_cloud(swarm)
+    point_cloud = pv.PolyData(points)
+    with swarm.access():
+        point_cloud.point_data["M"] = material.data.copy()
+
+    pl = pv.Plotter(window_size=(1000, 750))
+
+    pl.add_mesh(pvmesh, "Black", "wireframe")
+
+    # pl.add_points(point_cloud, color="Black",
+    #                   render_points_as_spheres=False,
+    #                   point_size=2.5, opacity=0.75)
+
+    pl.add_mesh(
+        point_cloud,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=False,
+        scalars="M",
+        use_transparency=False,
+        opacity=0.95,
+    )
+
+    pl.show(cpos="xy")
+
+
+if render == True:
+    plot_mat()
+
+
+
+
+
+
+

Function to save output of model#

+

Saves both the mesh vars and swarm vars

+
+
+
def saveData(step, outputPath):
+    mesh.petsc_save_checkpoint(
+        meshVars=[v, p, strain_rate_inv2, node_viscosity],
+        index=step,
+        outputPath=outputPath,
+    )
+
+    swarm.petsc_save_checkpoint(swarmName="swarm", index=step, outputPath=outputPath)
+
+
+
+
+
+

Density#

+
+
+
mantleDensity = 0.5
+slabDensity = 1.0
+
+density_fn = material.createMask(
+    [mantleDensity, mantleDensity, slabDensity, slabDensity, slabDensity]
+)
+
+
+stokes.bodyforce = Matrix([0, -1 * density_fn])
+
+
+
+
+
+
+
+

Boundary conditions#

+

Free slip by only constraining one component of velocity

+
+
+
# free slip
+stokes.add_dirichlet_bc(
+    (0.0, 0.0), "Left", (0)
+)  # left/right: function, boundaries, components
+stokes.add_dirichlet_bc((0.0, 0.0), "Right", (0))
+
+stokes.add_dirichlet_bc((0.0, 0.0), "Top", (1))
+stokes.add_dirichlet_bc(
+    (0.0, 0.0), "Bottom", (1)
+)  # top/bottom: function, boundaries, components
+
+
+
+
+
+

initial first guess of constant viscosity#

+
+
+
if uw.mpi.size == 1:
+    stokes.petsc_options["pc_type"] = "lu"
+
+stokes.petsc_options["snes_max_it"] = 500
+
+stokes.tolerance = 1e-6
+
+
+
+
+
+
+
### initial linear solve
+# stokes.constitutive_model.Parameters.viscosity = 1.0
+# stokes.saddle_preconditioner = 1 / stokes.constitutive_model.Parameters.viscosity
+
+stokes.constitutive_model.Parameters.shear_viscosity_0 = 1.0
+stokes.saddle_preconditioner = 1 / stokes.constitutive_model.Parameters.shear_viscosity_0
+stokes.solve(zero_init_guess=True)
+
+
+
+
+
+
+

add in NL rheology for solve loop#

+

CM-VP mockup

+
+
+
### viscosity from UW2 example
+upperMantleViscosity = 1.0
+lowerMantleViscosity = 100.0
+slabViscosity = 500.0
+coreViscosity = 500.0
+
+
+strainRate_2ndInvariant = stokes.Unknowns.Einv2
+
+
+
+
+
+
+
import sympy
+
+
+
+
+
+
+
### set background/initial viscoisity
+shear_viscosity_0 = [
+    upperMantleViscosity,
+    lowerMantleViscosity,
+    slabViscosity,
+    slabViscosity,
+    coreViscosity,
+]
+
+yield_stress = [0, 0, 0.06, 0.06, 0]
+
+
+
+
+
+
+
# stokes.constitutive_model = uw.constitutive_models.ViscoPlasticFlowModel
+
+
+
+
+
+
+
# stokes.constitutive_model.Parameters.shear_viscosity_0 = shear_viscosity_0
+# # stokes.constitutive_model.Parameters.viscosity
+
+
+
+
+
+
+
# stokes.constitutive_model.Parameters.materialIndex = material
+# # stokes.constitutive_model.Parameters.viscosity
+
+
+
+
+
+
+
# stokes.constitutive_model.Parameters.yield_stress = np.array(yield_stress)
+# # stokes.constitutive_model.Parameters.viscosity
+
+
+
+
+
+
+
# stokes.constitutive_model.Parameters.strainrate_inv_II = stokes.Unknowns.Einv2
+# # stokes.constitutive_model.Parameters.viscosity
+
+
+
+
+
+
+
# stokes.constitutive_model.Parameters.strainrate_inv_II_min = 1.0e-18
+# # stokes.constitutive_model.Parameters.viscosity
+
+
+
+
+
+
+
# stokes.constitutive_model.Parameters.yield_stress_min = np.array(yield_stress)*0.001
+# stokes.constitutive_model.Parameters.viscosity
+
+
+
+
+
+
+
# stokes.constitutive_model.Parameters.yield_stress = np.array(yield_stress)*mesh.X[1]
+# stokes.constitutive_model.Parameters.viscosity
+
+
+
+
+
+
+
stokes.constitutive_model.Parameters.averaging_method = "min"
+# stokes.constitutive_model.Parameters.viscosity
+
+
+
+
+
+
+
+

Main loop#

+

Stokes solve loop

+
+
+
step = 0
+max_steps = 1 #50
+time = 0
+
+
+# timing setup
+# viewer.getTimestep()
+# viewer.setTimestep(1)
+
+
+while step < max_steps:
+    print(f"\nstep: {step}, time: {time}")
+
+    # viz for parallel case - write the hdf5s/xdmfs
+    if step % 10 == 0:
+        if uw.mpi.rank == 0:
+            print(f"\nSave data: ")
+
+        ### updates projection of fields to the mesh
+        updateFields()
+
+        ### saves the mesh and swarm
+        saveData(step, expt_name)
+
+    if uw.mpi.rank == 0:
+        print(f"\nStokes solve: ")
+
+    stokes.solve(zero_init_guess=False)
+
+    ### get the timestep
+    dt = stokes.estimate_dt()
+
+    ### advect the particles according to the timestep
+    swarm.advection(V_fn=stokes.u.sym, delta_t=dt, corrector=False)
+
+    step += 1
+
+    time += dt
+
+
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Sandbox-VEP/Readme.html b/main/Notebooks/Examples-Sandbox-VEP/Readme.html new file mode 100644 index 0000000..652e186 --- /dev/null +++ b/main/Notebooks/Examples-Sandbox-VEP/Readme.html @@ -0,0 +1,596 @@ + + + + + + + + + + + Visco-elastic-plastic models — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Visco-elastic-plastic models

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

Visco-elastic-plastic models#

+

a.k.a. Digital Sandbox

+
+

Recent solver and visualization updates#

+
    +
  • [x] Ex_Compression_Example.py

  • +
  • [x] Ex_Shear_Band_Notch_Benchmark.py

  • +
  • [x] Ex_Shear_Band_Plasticity_PS.py

  • +
  • [ ] Ex_Shear_Band_Plasticity_SS.py

    +
      +
    • [ ] Not working properly

    • +
    +
  • +
  • [ ] Ex_Sheared_Layer_Elastic.py

    +
      +
    • [ ] need to update swarm.Lagrangian_Updater

    • +
    +
  • +
  • [ ] Ex_Sheared_Layer_Test.py

    +
      +
    • [ ] NameError: name ‘strain’ is not defined

    • +
    +
  • +
  • [ ] Ex_Sheared_Layer_Visualisation.py

    +
      +
    • [ ] Input files not found

    • +
    +
  • +
  • [x] Ex_VP-CM_slabSubduction.py

  • +
  • [ ] Theory_VE_NavierStokes.py

    +
      +
    • [ ] NameError: name ‘tauY’ is not defined

    • +
    +
  • +
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Sandbox-VEP/Theory_VE_NavierStokes.html b/main/Notebooks/Examples-Sandbox-VEP/Theory_VE_NavierStokes.html new file mode 100644 index 0000000..fde904e --- /dev/null +++ b/main/Notebooks/Examples-Sandbox-VEP/Theory_VE_NavierStokes.html @@ -0,0 +1,1477 @@ + + + + + + + + + + + Viscoelastic Navier-Stokes equation — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Viscoelastic Navier-Stokes equation

+ +
+ +
+
+ + + + +
+ +
+

Viscoelastic Navier-Stokes equation#

+

Here we outline how we combine the numerical NS scheme and the numerical Visco-elastic scheme

+ +
+

Viscoelasticity#

+

In viscoelasticity, the elastic part of the deformation is related to the stress rate. If we approach this problem as a perturbation to the viscous Navier-Stokes equation, we first consider the constitutive behaviour

+
+\[ + \frac{1}{2\mu}\frac{D{\boldsymbol\tau}}{Dt} + \frac{\boldsymbol\tau}{2\eta} = \dot{\boldsymbol\varepsilon} +\]
+

A first order difference form for \({D \tau}/{D t}\) then gives

+
+\[ + \frac{\boldsymbol\tau - \boldsymbol\tau^{*}}{2 \Delta t \mu} + \frac{\boldsymbol\tau}{2 \eta} = \dot{\boldsymbol\varepsilon} +\]
+

where \(\tau^*\) is the stress history along the characteristics associated with the current computational points. Rearranging to find an expression for the current stress in terms of the strain rate:

+
+\[ + \boldsymbol\tau = 2 \dot\varepsilon \eta_{\textrm{eff}_{(1)}} + \frac{\eta \boldsymbol\tau^{*}}{\Delta t \mu + \eta} +\]
+

where an ‘effective viscosity’ is introduced, defined as follows:

+
+\[ + \eta_{\textrm{eff}_{(1)}} = \frac{\Delta t \eta \mu}{\Delta t \mu + \eta} +\]
+

Substituting this definition of the stress into the forward-Euler form of the Navier-Stokes discretisation then gives

+
+\[ + \rho\frac{\mathbf{u}_{[1]} - \mathbf{u}^*}{\Delta t} = \rho g - \nabla \cdot \left[ 2 \dot{\boldsymbol\varepsilon}\eta_{\textrm{eff}_{(1)}} + \frac{\eta \boldsymbol\tau^{*}}{\Delta t \mu + \eta} \right] + \nabla p +\]
+

and the 2nd order (Crank-Nicholson) form becomes

+
+\[ + \rho\frac{\mathbf{u}_{[2]} - \mathbf{u}^*}{\Delta t} = \rho g - \frac{1}{2} \nabla \cdot \left[ 2 \dot\varepsilon \eta_{\textrm{eff}_{(1)}} + \left[\frac{\eta}{\Delta t \mu + \eta} + 1\right]\tau^* \right] + \nabla p +\]
+

If we use \(\tau^{**}\) in the estimate for the stress rate, we have

+
+\[ + \frac{3 \tau - 4 \tau^{*} + \tau^{**}}{4 \Delta t \mu} + \frac{\tau}{2 \eta} = \dot\varepsilon +\]
+

Giving

+
+\[ + \boldsymbol\tau = 2 \dot{\boldsymbol\varepsilon} \eta_{\textrm{eff}_{(2)}} + \frac{4 \eta \boldsymbol\tau^{*}}{2 \Delta t \mu + 3 \eta} - \frac{\eta \boldsymbol\tau^{**}}{2 \Delta t \mu + 3 \eta} +\]
+
+\[ + \eta_{\textrm{eff}_{(2)}} = \frac{2 \Delta t \eta \mu}{2 \Delta t \mu + 3 \eta} +\]
+
+\[ + \frac{\mathbf{u}_{[3]} - \mathbf{u}^*}{\Delta t} = \rho g + - \nabla \cdot \left[ \frac{5 \dot{\boldsymbol\varepsilon} \eta_{\textrm{eff_(2)}}}{6} + + \frac{5 \eta \boldsymbol\tau^{*}}{3 \cdot \left(2 \Delta t \mu + 3 \eta\right)} + \frac{2\boldsymbol\tau^{*}}{3} + - \frac{5 \eta \boldsymbol\tau^{**}}{12 \cdot \left(2 \Delta t \mu + 3 \eta\right)} - \frac{\boldsymbol\tau^{**}}{12} + \right] + \nabla p +\]
+
+
+\[ +\nabla\cdot\left[ \color{blue}{ \boldsymbol{\tau} - p \boldsymbol{I} } \right] = \color{green}{\frac{D \boldsymbol{u}}{Dt}} - \rho g +\]
+
+\[ +\frac{5 \dot\varepsilon \eta_\textrm{eff}}{6} + \frac{5 \eta \tau^{*}}{3 \cdot \left(2 \Delta\,\!t \mu + 3 \eta\right)} - \frac{5 \eta \tau^{**}}{12 \cdot \left(2 \Delta\,\!t \mu + 3 \eta\right)} + \frac{2 \tau^{*}}{3} - \frac{\tau^{**}}{12} +\]
+
+
+

Mock up … BDF / Adams-Moulton coefficients#

+

dot_f term will need BDf coefficients (https://en.wikipedia.org/wiki/Backward_differentiation_formula)

+

Flux history for Adams Moulton +Substitute for present stress in ADM

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import os
+
+
+
+
+
+
+
os.path.join("", "test")
+
+
+
+
+
+
+

+# Symbolic: sympy + uw3
+
+import os
+
+os.environ["UW_TIMING_ENABLE"] = "1"
+
+import petsc4py
+import underworld3 as uw
+import numpy as np
+import sympy
+import pyvista as pv
+import vtk
+
+from underworld3 import timing
+
+resolution = uw.options.getReal("model_resolution", default=0.033)
+mu = uw.options.getInt("mu", default=0.5)
+maxsteps = uw.options.getInt("max_steps", default=500)
+
+
+
+
+
+
+
mesh1 = uw.meshing.UnstructuredSimplexBox(
+    minCoords=(-1.5, -0.5),
+    maxCoords=(+1.5, +0.5),
+    cellSize=resolution,
+)
+
+x, y = mesh1.X
+
+
+
+
+
+
+
U = uw.discretisation.MeshVariable(
+    "U", mesh1, mesh1.dim, vtype=uw.VarType.VECTOR, degree=2
+)
+P = uw.discretisation.MeshVariable(
+    "P", mesh1, 1, vtype=uw.VarType.SCALAR, degree=1, continuous=True
+)
+T = uw.discretisation.MeshVariable("T", mesh1, 1, vtype=uw.VarType.SCALAR, degree=3)
+
+# Nodal values of deviatoric stress (symmetric tensor)
+
+work = uw.discretisation.MeshVariable(
+    "W", mesh1, 1, vtype=uw.VarType.SCALAR, degree=2, continuous=False
+)
+St = uw.discretisation.MeshVariable(
+    r"Stress",
+    mesh1,
+    (2, 2),
+    vtype=uw.VarType.SYM_TENSOR,
+    degree=2,
+    continuous=False,
+    varsymbol=r"{\tau}",
+)
+
+# May need these
+Edot_inv_II = uw.discretisation.MeshVariable(
+    "eps_II",
+    mesh1,
+    1,
+    vtype=uw.VarType.SCALAR,
+    degree=2,
+    varsymbol=r"{|\dot\varepsilon|}",
+)
+St_inv_II = uw.discretisation.MeshVariable(
+    "tau_II", mesh1, 1, vtype=uw.VarType.SCALAR, degree=2, varsymbol=r"{|\tau|}"
+)
+
+
+
+
+
+
+
swarm = uw.swarm.Swarm(mesh=mesh1, recycle_rate=5)
+
+material = uw.swarm.SwarmVariable(
+    "M",
+    swarm,
+    size=1,
+    vtype=uw.VarType.SCALAR,
+    proxy_continuous=True,
+    proxy_degree=1,
+    dtype=int,
+)
+
+strain_inv_II = uw.swarm.SwarmVariable(
+    "Strain",
+    swarm,
+    size=1,
+    vtype=uw.VarType.SCALAR,
+    proxy_continuous=True,
+    proxy_degree=2,
+    varsymbol=r"{|\varepsilon|}",
+    dtype=float,
+)
+
+stress_star = uw.swarm.SwarmVariable(
+    r"stress_dt",
+    swarm,
+    (2, 2),
+    vtype=uw.VarType.SYM_TENSOR,
+    proxy_continuous=True,
+    proxy_degree=2,
+    varsymbol=r"{\tau^{*}_{p}}",
+)
+
+stress_star_star = uw.swarm.SwarmVariable(
+    r"stress_2dt",
+    swarm,
+    (2, 2),
+    vtype=uw.VarType.SYM_TENSOR,
+    proxy_continuous=True,
+    proxy_degree=2,
+    varsymbol=r"{\tau^{**}_{p}}",
+)
+
+swarm.populate(fill_param=2)
+
+
+
+
+
+
+
stokes = uw.systems.Stokes(
+    mesh1,
+    velocityField=U,
+    pressureField=P,
+    verbose=False,
+    solver_name="stokes",
+)
+
+stokes
+
+
+
+
+
+
+
uw.systems.Stokes
+
+
+
+
+
+
+
from sympy import UnevaluatedExpr
+
+st = sympy.UnevaluatedExpr(stokes.constitutive_model.viscosity) * sympy.UnevaluatedExpr(
+    stokes.strainrate
+)
+st
+
+
+
+
+
+
+
eta_0 = sympy.sympify(10) ** -6
+C_0 = sympy.log(10**6)
+
+
+stokes.constitutive_model = uw.constitutive_models.ViscoElasticPlasticFlowModel
+stokes.constitutive_model.Parameters.shear_viscosity_0 = sympy.symbols(r"\eta")
+stokes.constitutive_model.Parameters.shear_modulus = sympy.symbols(r"\mu")
+stokes.constitutive_model.Parameters.stress_star = stress_star.sym
+stokes.constitutive_model.Parameters.dt_elastic = sympy.symbols(
+    r"\Delta\ t"
+)  # sympy.sympify(1) / 10
+stokes.constitutive_model.Parameters.strainrate_inv_II = stokes.Unknowns.Einv2
+stokes.constitutive_model.Parameters.strainrate_inv_II_min = 0
+stokes.constitutive_model
+
+
+
+
+
+
+
stokes.constitutive_model.Parameters.viscosity
+
+
+
+
+
+
+
# Set solve options here (or remove default values
+# stokes.petsc_options.getAll()
+
+# Constant visc
+
+stokes.penalty = 0
+
+stokes.tolerance = 1.0e-4
+
+# Velocity boundary conditions
+
+stokes.add_dirichlet_bc((0.0, 0.0), "Inclusion", (0, 1))
+stokes.add_dirichlet_bc((1.0, 0.0), "Top", (0, 1))
+stokes.add_dirichlet_bc((-1.0, 0.0), "Bottom", (0, 1))
+stokes.add_dirichlet_bc((0.0), "Left", (1))
+stokes.add_dirichlet_bc((0.0), "Right", (1))
+
+
+
+
+
+
+
stokes._setup_problem_description()
+
+
+
+
+
+
+
t0 = St.sym
+t1 = stress_star.sym
+t0 + t1
+
+
+
+
+
+
+
t2 = t0 + t1
+t2
+
+t2_II_sq = (t2**2).trace() / 2
+t2_II = sympy.sqrt(t2_II_sq)
+
+overshoot = sympy.simplify(sympy.sqrt(t2_II_sq) / tauY)
+overshoot
+
+
+
+
+
+
+
t2_II_sq
+
+
+
+
+
+
+
stokes.constitutive_model.flux(stokes.strainrate)
+
+
+
+
+
+
+
# Crank-Nicholson timestep - Jacobians
+(stokes.constitutive_model.flux(stokes.strainrate) / 2 + stress_star.sym / 2).diff(
+    stokes._u.sym[0]
+)
+
+
+
+
+
+
+
(stokes.stress_deviator_1d / 2 + stress_star.sym_1d / 2)[0]
+
+
+
+
+
+
+
# RHS
+stokes._u_f0
+
+
+
+
+
+
+
# Jacobian terms
+stokes._u_f1[1, 1].diff(stokes._L[1, 1])
+
+
+
+
+
+
+
## Jacobians (e.g. stress rate derivatives with respect to strain rate tensor)
+
+stokes._uu_G3
+
+
+
+
+
+
+
## And now, second order terms
+
+stokes.constitutive_model = uw.constitutive_models.ViscoElasticPlasticFlowModel(
+    mesh1.dim
+)
+stokes.constitutive_model.Parameters.shear_viscosity_0 = eta_0 * sympy.exp(
+    -C_0 * T.sym[0]
+)
+stokes.constitutive_model.Parameters.shear_modulus = 100
+stokes.constitutive_model.Parameters.stress_star = stress_star.sym
+stokes.constitutive_model.Parameters.stress_star_star = stress_star_star.sym
+stokes.constitutive_model.Parameters.dt_elastic = sympy.sympify(1) / 10
+stokes.constitutive_model.Parameters.strainrate_inv_II = stokes._Einv2
+stokes.constitutive_model.Parameters.strainrate_inv_II_min = 0
+
+stokes.saddle_preconditioner = 1 / stokes.constitutive_model.Parameters.viscosity
+
+stokes.constitutive_model
+
+
+
+
+
+
+
stokes.stress_deviator_1d[2]
+
+
+
+
+
+
+
0 / 0
+
+
+
+
+
+
+
stokes.solve()
+
+
+
+
+
+
+
# Adams / Bashforth & Adams Moulton ...
+
+s = sympy.Symbol(r"\tau")
+s1 = sympy.Symbol(r"\tau^*")
+s2 = sympy.Symbol(r"\tau^**")
+dt = sympy.Symbol(r"\Delta\,\!t")
+mu = sympy.Symbol(r"\mu")
+eta = sympy.Symbol(r"\eta")
+eta_p = sympy.Symbol(r"\eta_p")
+eta_eff = sympy.Symbol(r"\eta_\textrm{eff}")
+edot = sympy.Symbol(r"\dot\varepsilon")
+tr = sympy.Symbol(r"t_r")
+
+# Stress history
+
+# 1st order difference expression for stress rate
+sdot1 = (s - s1) / dt
+
+# 2nd order difference for stress rate
+sdot2 = (3 * s - 4 * s1 + s2) / (2 * dt)
+
+
+
+
+
+
+
display(sdot1)
+display(sdot1 / (2 * mu) + s / (2 * eta))  # + s / (2 * eta_p))
+print(sympy.latex(sdot1 / (2 * mu) + s / (2 * eta) + s / (2 * eta_p)))
+Seq1 = sympy.Equality(
+    sympy.simplify(sdot1 / (2 * mu) + s / (2 * eta) + 0 * s / (2 * eta_p)), edot
+)
+display(Seq1)
+
+# solve this for an expression in terms of the present stress, $\tau$
+a = sympy.simplify(sympy.solve(Seq1, s)[0]).expand()
+display(a)
+
+# b = sympy.simplify(sympy.solve(Seq1, eta_p)[0]).expand()
+# display(b)
+
+
+
+
+
+
+
S = a.subs(edot, stokes.strainrate).subs(s1, stress_star.sym)
+SII = sympy.simplify((S**2).trace())
+
+
+
+
+
+
+
## yielding case
+
+tauY = sympy.symbols(r"\tau_y")
+
+
+
+
+
+
+
etaY2 = tauY / (2 * edot + (s1 - tauY) / (mu * dt))
+etaY2
+
+
+
+
+
+
+
sympy.simplify(etaY - etaY2)
+
+
+
+
+
+
+
stokes.constitutive_model.Parameters.ve_effective_viscosity
+
+
+
+
+
+
+
sympy.simplify(a / 2 + s1 / 2).expand().collect(s1)
+
+
+
+
+
+
+
# Identify effective viscosity
+
+eta_eff_1 = sympy.simplify(eta * mu * dt / (mu * dt + eta))
+display(eta_eff_1)
+print(sympy.latex(eta_eff_1))
+
+# rename this in the equations
+b = a.subs(eta_eff_1, eta_eff)
+display(b)
+print(sympy.latex(b))
+
+## An equivalent form for this is
+
+c = 2 * eta_eff * edot + (s1 * tr / (dt + tr))
+display(c)
+
+## Validate that
+sympy.simplify(b - c.subs(tr, eta / mu))
+
+(b / 2 + s1 / 2)
+
+
+
+
+
+
+
# Now we can try a 2nd order
+
+display(sdot2)
+print(sympy.latex(sdot2))
+display(sdot2 / (2 * mu) + s / (2 * eta))
+print(sympy.latex(sdot2 / (2 * mu) + s / (2 * eta)))
+Seq2 = sympy.Equality(sympy.simplify(sdot2 / (2 * mu) + s / (2 * eta)), edot)
+display(Seq2)
+sympy.simplify(sympy.solve(Seq2, s)[0])
+
+
+
+
+
+
+
eta_eff_2 = sympy.simplify(2 * eta * mu * dt / (2 * mu * dt + 3 * eta))
+display(eta_eff_2)
+print(sympy.latex(eta_eff_2))
+
+
+sympy.simplify(2 * eta * sympy.solve(Seq2, s)[0] / (2 * eta_eff_2))
+
+# solve this for an expression in terms of the present stress, $\tau$
+a2 = sympy.simplify(sympy.solve(Seq2, s)[0]).expand()
+display(a2)
+
+# Identify effective viscosity
+
+
+# rename this in the equations
+b2 = a2.subs(eta_eff_2, eta_eff)
+display(b2)
+print(sympy.latex(b2))
+
+## And this is what happens in Adams-Moulton 3rd order
+
+display(5 * b2 / 12 + 2 * s1 / 3 - s2 / 12)
+print(sympy.latex(5 * b2 / 12 + 2 * s1 / 3 - s2 / 12))
+
+
+
+
+
+
+
a = sympy.simplify(2 * eta * sympy.solve(Seq2, s)[0] / (2 * eta_eff_2))
+tau_2 = a.expand().subs(eta / mu, tr)
+
+
+
+
+
+
+
a
+
+
+
+
+
+
+
0 / 0
+
+
+
+
+
+
+
stokes.constitutive_model.Parameters.yield_stress.subs(
+    ((strain.sym[0], 0.25), (y, 0.0))
+)
+
+
+
+
+
+
+
stokes.constitutive_model.Parameters.viscosity
+
+
+
+
+
+
+
def return_points_to_domain(coords):
+    new_coords = coords.copy()
+    new_coords[:, 0] = (coords[:, 0] + 1.5) % 3 - 1.5
+    return new_coords
+
+
+
+
+
+
+
ts = 0
+
+
+
+
+
+
+
delta_t = stokes.estimate_dt()
+
+expt_name = f"output/shear_band_sw_nonp_{mu}"
+
+for step in range(0, 10):
+    stokes.solve(zero_init_guess=False)
+
+    nodal_strain_rate_inv2.uw_function = sympy.Max(
+        0.0,
+        stokes._Einv2
+        - 0.5
+        * stokes.constitutive_model.Parameters.yield_stress
+        / stokes.constitutive_model.Parameters.bg_viscosity,
+    )
+    nodal_strain_rate_inv2.solve()
+
+    with mesh1.access(strain_rate_inv2_p):
+        strain_rate_inv2_p.data[...] = strain_rate_inv2.data.copy()
+
+    nodal_strain_rate_inv2.uw_function = stokes._Einv2
+    nodal_strain_rate_inv2.solve()
+
+    with swarm.access(strain), mesh1.access():
+        XX = swarm.particle_coordinates.data[:, 0]
+        YY = swarm.particle_coordinates.data[:, 1]
+        mask = (2 * XX / 3) ** 4  # * 1.0 - (YY * 2)**8
+        strain.data[:, 0] += (
+            delta_t * mask * strain_rate_inv2_p.rbf_interpolate(swarm.data)[:, 0]
+            - 0.1 * delta_t
+        )
+        strain_dat = (
+            delta_t * mask * strain_rate_inv2_p.rbf_interpolate(swarm.data)[:, 0]
+        )
+        print(
+            f"dStrain / dt = {delta_t * (mask * strain_rate_inv2_p.rbf_interpolate(swarm.data)[:,0]).mean()}, {delta_t}"
+        )
+
+    mesh1.write_timestep_xdmf(
+        f"{expt_name}",
+        meshUpdates=False,
+        meshVars=[p_soln, v_soln, strain_rate_inv2_p],
+        swarmVars=[strain],
+        index=ts,
+    )
+
+    swarm.save(f"{expt_name}.swarm.{ts}.h5")
+    strain.save(f"{expt_name}.strain.{ts}.h5")
+
+    # Update the swarm locations
+    swarm.advection(v_soln.sym, delta_t=delta_t, restore_points_to_domain_func=None)
+
+    if uw.mpi.rank == 0:
+        print("Timestep {}, dt {}".format(step, delta_t))
+
+    ts += 1
+
+
+
+
+
+
+
nodal_visc_calc.uw_function = sympy.log(stokes.constitutive_model.Parameters.viscosity)
+nodal_visc_calc.solve()
+
+yield_stress_calc.uw_function = stokes.constitutive_model.Parameters.yield_stress
+yield_stress_calc.solve()
+
+nodal_tau_inv2.uw_function = (
+    2 * stokes.constitutive_model.Parameters.viscosity * stokes._Einv2
+)
+nodal_tau_inv2.solve()
+
+
+
+
+
+
+
# check it - NOTE - for the periodic mesh, points which have crossed the coordinate sheet are plotted somewhere
+# unexpected. This is a limitation we are stuck with for the moment.
+
+if uw.mpi.size == 1:
+    
+    import underworld3.visualisation as vis # use tools from here
+    
+    mesh1.vtk("tmp_shear_inclusion.vtk")
+    pvmesh = pv.read("tmp_shear_inclusion.vtk")
+
+    pvpoints = pvmesh.points[:, 0:2]
+    usol = v_soln.rbf_interpolate(pvpoints)
+
+    pvmesh.point_data["P"] = p_soln.rbf_interpolate(pvpoints)
+    pvmesh.point_data["Edot"] = strain_rate_inv2.rbf_interpolate(pvpoints)
+    pvmesh.point_data["Visc"] = np.exp(node_viscosity.rbf_interpolate(pvpoints))
+    pvmesh.point_data["Edotp"] = strain_rate_inv2_p.rbf_interpolate(pvpoints)
+    pvmesh.point_data["Strs"] = dev_stress_inv2.rbf_interpolate(pvpoints)
+    pvmesh.point_data["StrY"] = yield_stress.rbf_interpolate(pvpoints)
+    pvmesh.point_data["dStrY"] = (
+        pvmesh.point_data["StrY"]
+        - 2 * pvmesh.point_data["Visc"] * pvmesh.point_data["Edot"]
+    )
+    pvmesh.point_data["Mat"] = material.rbf_interpolate(pvpoints)
+    pvmesh.point_data["Strn"] = strain._meshVar.rbf_interpolate(pvpoints)
+
+    # Velocity arrows
+
+    v_vectors = np.zeros_like(pvmesh.points)
+    v_vectors[:, 0:2] = v_soln.rbf_interpolate(pvpoints)
+
+    # Points (swarm)
+
+    with swarm.access():
+        plot_points = np.where(strain.data > 0.0001)
+        strain_data = strain.data.copy()
+
+        points = np.zeros((swarm.data[plot_points].shape[0], 3))
+        points[:, 0] = swarm.data[plot_points[0], 0]
+        points[:, 1] = swarm.data[plot_points[0], 1]
+        point_cloud = pv.PolyData(points)
+        point_cloud.point_data["strain"] = strain.data[plot_points]
+
+    pl = pv.Plotter(window_size=(500, 500))
+
+    # pl.add_arrows(pvmesh.points, v_vectors, mag=0.1, opacity=0.75)
+    # pl.camera_position = "xy"
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="Blues",
+        edge_color="Grey",
+        show_edges=True,
+        # clim=[-1.0,1.0],
+        scalars="Edotp",
+        use_transparency=False,
+        opacity=0.5,
+    )
+
+    pl.add_points(
+        point_cloud,
+        colormap="Oranges",
+        scalars="strain",
+        point_size=10.0,
+        opacity=0.0,
+        # clim=[0.0,0.2],
+    )
+
+    pl.camera.SetPosition(0.0, 0.0, 3.0)
+    pl.camera.SetFocalPoint(0.0, 0.0, 0.0)
+    pl.camera.SetClippingRange(1.0, 8.0)
+
+    pl.show()
+
+
+
+
+
+
+
strain_dat.max()
+
+
+
+
+
+
+
alph = sympy.symbols(r"\alpha_:10")
+alph[9]
+
+
+fn = alph[0] * U[0].sym + alph[1] * U[1].sym
+
+
+
+
+
+
+
fn.diff(alph[0])
+
+
+
+
+
+
+
with swarm.access():
+    print(strain.data.max())
+
+
+
+
+
+
+
strain_rate_inv2_p.rbf_interpolate(mesh1.data).max()
+
+
+
+
+
+
+

#

+
+
+
mesh1._search_lengths
+
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Sandbox-VEP/output/README.html b/main/Notebooks/Examples-Sandbox-VEP/output/README.html new file mode 100644 index 0000000..d55d7a3 --- /dev/null +++ b/main/Notebooks/Examples-Sandbox-VEP/output/README.html @@ -0,0 +1,519 @@ + + + + + + + + + + + Sandbox ViscoElastic/Plastic model outputs — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Sandbox ViscoElastic/Plastic model outputs

+ +
+
+ +
+
+
+ + + + +
+ +
+

Sandbox ViscoElastic/Plastic model outputs#

+

These files are NOT under version control

+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-StokesFlow/Ex_ChannelFlow_IrregularBase.html b/main/Notebooks/Examples-StokesFlow/Ex_ChannelFlow_IrregularBase.html new file mode 100644 index 0000000..8dc3a3a --- /dev/null +++ b/main/Notebooks/Examples-StokesFlow/Ex_ChannelFlow_IrregularBase.html @@ -0,0 +1,938 @@ + + + + + + + + + + + 3D channel flow — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

3D channel flow

+ +
+
+ +
+
+
+ + + + +
+ +
+

3D channel flow#

+

Potentially applicable to ice-sheet flow models

+
+
+
import underworld3 as uw
+import numpy as np
+import sympy
+
+# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+import os
+import sys
+
+if uw.mpi.size == 1:
+    import matplotlib.pyplot as plt 
+
+
+
+elements = 7
+resolution = 1/elements
+
+outputPath = f"./output/ChannelFlow3D"
+expt_name = f"WigglyBottom_{elements}"
+
+os.makedirs(outputPath, exist_ok=True)
+
+
+
+
+
+
+
## This is adapted from terrain mesh example provided
+## in the gmsh examples.
+
+from enum import Enum
+
+class boundaries(Enum):
+    Upper = 1
+    Lower = 2
+    Left  = 3
+    Right = 4
+    Front = 5
+    Back  = 6
+    All_Boundaries = 1001 # Petsc Boundary Label
+
+import gmsh
+import math
+import sys
+
+gmsh.initialize()
+gmsh.option.setNumber('General.Verbosity', 1)
+
+gmsh.model.add("terrain")
+
+# create the terrain surface from N x N input data points (here simulated using
+# a simple function):
+N = 200
+coords = []  # x, y, z coordinates of all the points
+nodes = []  # tags of corresponding nodes
+tris = []  # connectivities (node tags) of triangle elements
+lin = [[], [], [], []]  # connectivities of boundary line elements
+
+
+def tag(i, j):
+    return (N + 1) * i + j + 1
+
+for i in range(N + 1):
+    X = float(i) / N
+    for j in range(N + 1):
+        Y = float(j) / N
+        
+        nodes.append(tag(i, j))
+        coords.extend([
+            2 * X,
+            Y, 
+            0.05 * math.sin(20 * (X + 0.1 * Y)) * math.cos(sympy.pi * Y) - 0.1 * math.sin(sympy.pi * Y)
+        ])
+        if i > 0 and j > 0:
+            tris.extend([tag(i - 1, j - 1), tag(i, j - 1), tag(i - 1, j)])
+            tris.extend([tag(i, j - 1), tag(i, j), tag(i - 1, j)])
+        if (i == 0 or i == N) and j > 0:
+            lin[3 if i == 0 else 1].extend([tag(i, j - 1), tag(i, j)])
+        if (j == 0 or j == N) and i > 0:
+            lin[0 if j == 0 else 2].extend([tag(i - 1, j), tag(i, j)])
+pnt = [tag(0, 0), tag(N, 0), tag(N, N), tag(0, N)]  # corner points element
+
+# create 4 corner points
+gmsh.model.geo.addPoint(0, 0, coords[3 * tag(0, 0) - 1], 1)
+gmsh.model.geo.addPoint(2, 0, coords[3 * tag(N, 0) - 1], 2)
+gmsh.model.geo.addPoint(2, 1, coords[3 * tag(N, N) - 1], 3)
+gmsh.model.geo.addPoint(0, 1, coords[3 * tag(0, N) - 1], 4)
+gmsh.model.geo.synchronize()
+
+# create 4 discrete bounding curves, with their boundary points
+for i in range(4):
+    gmsh.model.addDiscreteEntity(1, i + 1, [i + 1, i + 2 if i < 3 else 1])
+
+# create one discrete surface, with its bounding curves
+topo = gmsh.model.addDiscreteEntity(2, 1, [1, 2, -3, -4])
+
+# add all the nodes on the surface (for simplicity... see below)
+gmsh.model.mesh.addNodes(2, 1, nodes, coords)
+
+# add elements on the 4 points, the 4 curves and the surface
+for i in range(4):
+    # type 15 for point elements:
+    gmsh.model.mesh.addElementsByType(i + 1, 15, [], [pnt[i]])
+    # type 1 for 2-node line elements:
+    gmsh.model.mesh.addElementsByType(i + 1, 1, [], lin[i])
+# type 2 for 3-node triangle elements:
+gmsh.model.mesh.addElementsByType(1, 2, [], tris)
+
+# reclassify the nodes on the curves and the points (since we put them all on
+# the surface before for simplicity)
+gmsh.model.mesh.reclassifyNodes()
+
+# note that for more complicated meshes, e.g. for on input unstructured STL, we
+# could use gmsh.model.mesh.classifySurfaces() to automatically create the
+# discrete entities and the topology; but we would have to extract the
+# boundaries afterwards
+
+# create a geometry for the discrete curves and surfaces, so that we can remesh
+# them
+
+gmsh.model.mesh.createGeometry()
+
+# create other CAD entities to form one volume below the terrain surface, and
+# one volume on top; beware that only built-in CAD entities can be hybrid,
+# i.e. have discrete entities on their boundary: OpenCASCADE does not support
+# this feature
+
+p5 = gmsh.model.geo.addPoint(0, 0, 0.5)
+p6 = gmsh.model.geo.addPoint(2, 0, 0.5)
+p7 = gmsh.model.geo.addPoint(2, 1, 0.5)
+p8 = gmsh.model.geo.addPoint(0, 1, 0.5)
+
+c5 = gmsh.model.geo.addLine(p5, p6)
+c6 = gmsh.model.geo.addLine(p6, p7)
+c7 = gmsh.model.geo.addLine(p7, p8)
+c8 = gmsh.model.geo.addLine(p8, p5)
+
+c14 = gmsh.model.geo.addLine(1, p5)
+c15 = gmsh.model.geo.addLine(2, p6)
+c16 = gmsh.model.geo.addLine(3, p7)
+c17 = gmsh.model.geo.addLine(4, p8)
+
+# bottom and top
+# ll1 = gmsh.model.geo.addCurveLoop([c1, c2, c3, c4])
+# s1 = gmsh.model.geo.addPlaneSurface([ll1])
+
+ll2 = gmsh.model.geo.addCurveLoop([c5, c6, c7, c8])
+s2 = gmsh.model.geo.addPlaneSurface([ll2])
+
+# upper
+ll7 = gmsh.model.geo.addCurveLoop([c5, -c15, -1, c14])
+s7 = gmsh.model.geo.addPlaneSurface([ll7])
+ll8 = gmsh.model.geo.addCurveLoop([c6, -c16, -2, c15])
+s8 = gmsh.model.geo.addPlaneSurface([ll8])
+ll9 = gmsh.model.geo.addCurveLoop([c7, -c17, 3, c16])
+s9 = gmsh.model.geo.addPlaneSurface([ll9])
+ll10 = gmsh.model.geo.addCurveLoop([c8, -c14, 4, c17])
+s10 = gmsh.model.geo.addPlaneSurface([ll10])
+
+sl2 = gmsh.model.geo.addSurfaceLoop([s2, s7, s8, s9, s10, 1])
+v2 = gmsh.model.geo.addVolume([sl2])
+
+gmsh.model.geo.synchronize()
+
+gmsh.model.addPhysicalGroup(2, [s2], boundaries.Upper.value, name=boundaries.Upper.name,)
+gmsh.model.addPhysicalGroup(2, [s7], boundaries.Front.value, name=boundaries.Front.name,)
+gmsh.model.addPhysicalGroup(2, [s8], boundaries.Right.value, name=boundaries.Right.name,)
+gmsh.model.addPhysicalGroup(2, [s9], boundaries.Back.value, name=boundaries.Back.name,)
+gmsh.model.addPhysicalGroup(2, [s10], boundaries.Left.value, name=boundaries.Left.name,)
+
+gmsh.model.addPhysicalGroup(2, [topo], boundaries.Lower.value, name=boundaries.Lower.name,)
+
+gmsh.model.addPhysicalGroup(3, [v2], 666666, "Elements")
+
+gmsh.option.setNumber('Mesh.MeshSizeMin', resolution)
+gmsh.option.setNumber('Mesh.MeshSizeMax', resolution)
+gmsh.model.mesh.generate(3)
+
+gmsh.write('.meshes/tmp_terrain.msh')
+
+# gmsh.fltk.run()
+
+gmsh.finalize()
+
+
+
+
+
+
+
terrain_mesh = uw.discretisation.Mesh(
+        ".meshes/tmp_terrain.msh",
+        degree=1,
+        qdegree=3,
+        useMultipleTags=True,
+        useRegions=True,
+        markVertices=True,
+        boundaries=boundaries,
+        coordinate_system_type=None,
+        refinement=1,
+        refinement_callback=None,
+        return_coords_to_bounds=None,
+    )
+
+x,y,z = terrain_mesh.X
+
+
+
+
+
+
+
# l = terrain_mesh.dm.getLabel("Lower")
+# i = l.getStratumSize(2)
+# ii = uw.utilities.gather_data(np.array([float(i)]))
+
+# if uw.mpi.rank == 0:
+#     print(f"Nodes in LOWER by rank: {ii.astype(int)}", flush=True)
+
+# uw.mpi.barrier()
+
+
+
+
+
+
+
terrain_mesh.view()
+
+
+
+
+
+
+
0/0
+
+
+
+
+
+
+
terrain_mesh.dm.view()
+
+
+
+
+
+
+
n_vect = uw.discretisation.MeshVariable("Gamma", terrain_mesh, vtype=uw.VarType.VECTOR,
+                                        degree=2, varsymbol="{\Gamma_N}")
+
+projection = uw.systems.Vector_Projection(terrain_mesh, n_vect)
+projection.uw_function = sympy.Matrix([[0,0,0]])
+
+GammaNorm = 1 / sympy.sqrt(terrain_mesh.Gamma.dot(terrain_mesh.Gamma))
+
+projection.add_natural_bc(terrain_mesh.Gamma * GammaNorm, "Lower")
+projection.smoothing = 1.0e-6
+projection.solve(verbose=False)
+
+# Ensure n_vect are unit vectors 
+with terrain_mesh.access(n_vect):
+    n_vect.data[:,:] /= np.sqrt(n_vect.data[:,0]**2 + n_vect.data[:,1]**2 + n_vect.data[:,2]**2).reshape(-1,1)
+
+
+
+
+
+
+
with terrain_mesh.access(n_vect):
+    print(n_vect.data.max(), flush=True)
+
+
+
+
+
+
+
stokes = uw.systems.Stokes(terrain_mesh, solver_name="stokes_terrain")
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.shear_viscosity_0 = 1
+stokes.penalty = 1.0
+
+v = stokes.Unknowns.u
+p = stokes.Unknowns.p
+
+stokes.add_essential_bc( [sympy.oo, 0.0, 0.0 ], "Left") 
+stokes.add_essential_bc( [sympy.oo, 0.0, 0.0 ], "Right") 
+stokes.add_essential_bc( [0.0, 0.0, 0.0 ], "Front") 
+stokes.add_essential_bc( [0.0, 0.0, 0.0 ], "Back") 
+stokes.add_essential_bc( [sympy.oo, sympy.oo, 0.0 ], "Upper") 
+
+## Free slip base (conditional)
+Gamma = n_vect.sym # terrain_mesh.Gamma
+bc_mask0 = sympy.Piecewise((1.0, z < -0.09), (0.0, True))
+bc_mask1 = sympy.Piecewise((1.0, -0.09 < z ), (0.0, True))
+bc_mask2 = sympy.Piecewise((1.0, z < 0.0 ), (0.0, True))
+
+nbc = 10000 *  sympy.simplify( 
+                bc_mask0 * Gamma.dot(v.sym) *  Gamma +
+                (bc_mask1 * bc_mask2)  * v.sym 
+                )
+
+stokes.add_natural_bc(nbc, "Lower")
+
+## Buoyancy
+
+theta = 2 * sympy.pi / 180
+stokes.bodyforce = -sympy.Matrix([[sympy.sin(theta), 0.0, 0.0*sympy.cos(theta)]])
+
+stokes.petsc_options.setValue("ksp_monitor", None)
+stokes.petsc_options.setValue("snes_monitor", None)
+
+stokes.solve()
+
+
+
+
+
+
+
terrain_mesh.write_timestep(
+    expt_name,
+    meshUpdates=True,
+    meshVars=[p, v],
+    outputPath=outputPath,
+    index=0,
+)
+
+
+
+
+
+
+
## Visualise the mesh
+
+# OR
+# check the mesh if in a notebook / serial
+
+if uw.mpi.size == 1:
+
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(terrain_mesh)
+    pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v.sym)
+
+    velocity_points = vis.meshVariable_to_pv_cloud(v)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v.sym)
+
+    clipped = pvmesh.clip(origin=(0.0, 0.0, -0.09), normal=(0.0, 0, 1), invert=True)
+    clipped.point_data["V"] = vis.vector_fn_to_pv_points(clipped, v.sym)
+
+    clipped2 = pvmesh.clip(origin=(0.0, 0.0, -0.05), normal=(0.0, 0, 1), invert=True)
+    clipped2.point_data["V"] = vis.vector_fn_to_pv_points(clipped2, v.sym)
+    
+    clipped3 = pvmesh.clip(origin=(0.0, 0.0, 0.4), normal=(0.0, 0, 1), invert=False)
+    clipped3.point_data["V"] = vis.vector_fn_to_pv_points(clipped3, v.sym)
+
+
+    skip = 10
+    points = np.zeros((terrain_mesh._centroids[::skip].shape[0], 3))
+    points[:, 0] = terrain_mesh._centroids[::skip, 0]
+    points[:, 1] = terrain_mesh._centroids[::skip, 1]
+    points[:, 2] = terrain_mesh._centroids[::skip, 2]
+
+    point_cloud = pv.PolyData(points[np.logical_and(points[:, 0] < 2.0, points[:, 0] > 0.0)]  )
+
+    pvstream = pvmesh.streamlines_from_source(
+        point_cloud, vectors="V", 
+        integration_direction="forward", 
+        integrator_type=45,
+        surface_streamlines=False,
+        initial_step_length=0.1,
+        max_time=0.5,
+        max_steps=1000
+    )
+
+    point_cloud2 = pv.PolyData(points[np.logical_and(points[:, 2] < 0.5, points[:, 2] > 0.45)]  )
+
+    pvstream2 = pvmesh.streamlines_from_source(
+        point_cloud2, vectors="V", 
+        integration_direction="forward", 
+        integrator_type=45,
+        surface_streamlines=False,
+        initial_step_length=0.01,
+        max_time=0.5,
+        max_steps=1000
+    )
+
+    pl = pv.Plotter(window_size=[1000, 1000])
+    pl.add_axes()
+
+    pl.add_mesh(pvmesh,'Grey', 'wireframe', opacity=0.1)
+    pl.add_mesh(clipped,'Blue', show_edges=False, opacity=0.25)
+    # pl.add_mesh(pvmesh, 'white', show_edges=True, opacity=0.5)
+
+    #pl.add_mesh(pvstream)
+    pl.add_mesh(pvstream2)
+
+
+    arrows = pl.add_arrows(clipped2.points, clipped2.point_data["V"], 
+                           show_scalar_bar = False, opacity=1,
+                           mag=100, )
+    
+    # arrows = pl.add_arrows(clipped3.points, clipped3.point_data["V"], 
+    #                        show_scalar_bar = False, opacity=1,
+    #                        mag=33, )
+
+
+    # pl.screenshot(filename="sphere.png", window_size=(1000, 1000), return_img=False)
+    # OR
+    
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Cartesian_SolC.html b/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Cartesian_SolC.html new file mode 100644 index 0000000..ef53b09 --- /dev/null +++ b/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Cartesian_SolC.html @@ -0,0 +1,984 @@ + + + + + + + + + + + Stokes Benchmark SolCx — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Stokes Benchmark SolCx

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

Stokes Benchmark SolCx#

+
+
+
# %%
+import petsc4py
+from petsc4py import PETSc
+
+import nest_asyncio
+nest_asyncio.apply()
+
+# options = PETSc.Options()
+# options["help"] = None 
+
+import os
+os.environ["UW_TIMING_ENABLE"] = "1"
+
+
+
+
+
+
+
import underworld3 as uw
+from underworld3.systems import Stokes
+from underworld3 import function
+from underworld3 import timing
+
+import numpy as np
+import sympy
+from sympy import Piecewise
+
+
+
+
+
+
+
# %%
+n_els = 4
+refinement = 2
+
+mesh1 = uw.meshing.UnstructuredSimplexBox(regular=True,
+    minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0), cellSize=1 / n_els, 
+    qdegree=3, refinement=refinement
+)
+
+mesh2 = uw.meshing.StructuredQuadBox(
+    elementRes=(n_els, n_els),
+    minCoords=(0.0, 0.0), 
+    maxCoords=(1.0, 1.0), 
+    qdegree=3, 
+    refinement=refinement
+)
+
+mesh = mesh1
+x,y = mesh.X
+
+
+
+
+
+
+
stokes = uw.systems.Stokes(mesh, verbose=False)
+
+
+
+
+
+
+
v = stokes.Unknowns.u
+p = stokes.Unknowns.p
+
+stokes.constitutive_model=uw.constitutive_models.ViscousFlowModel(stokes.Unknowns)
+stokes.constitutive_model.Parameters.shear_viscosity_0 = 1
+# %%
+T = uw.discretisation.MeshVariable("T", mesh, 1, degree=3, continuous=True, varsymbol=r"{T}")
+T2 = uw.discretisation.MeshVariable("T2", mesh, 1, degree=3, continuous=True, varsymbol=r"{T_2}")
+
+v0 = stokes.Unknowns.u.clone("V0", r"{v_0}")
+v1 = v0.clone("V1", r"{v_1}")
+
+
+
+
+
+
+
eta_0 = 1.0
+x_c = 0.5
+f_0 = 1.0
+
+
+
+
+
+
+
stokes.penalty = 100.0
+stokes.bodyforce = sympy.Matrix(
+    [
+        0,
+        Piecewise(
+            (f_0, x > x_c),
+            (0.0, True),
+        ),
+    ]
+)
+
+
+
+
+
+
+
# This is the other way to impose no vertical flow
+
+# stokes.add_natural_bc(   [0.0,1e5*v.sym[1]], "Top")              # Top "free slip / penalty"
+
+
+
+
+
+
+
# free slip.
+
+stokes.add_dirichlet_bc((sympy.oo, 0.0), "Top")
+stokes.add_dirichlet_bc((sympy.oo,0.0), "Bottom")
+stokes.add_dirichlet_bc((0.0,sympy.oo), "Left")
+stokes.add_dirichlet_bc((0.0,sympy.oo), "Right")
+
+
+
+
+

We may need to adjust the tolerance if \(\Delta \eta\) is large

+
+
+
stokes.tolerance = 1.0e-6
+
+
+
+
+
+
+
stokes.petsc_options["snes_monitor"]= None
+stokes.petsc_options["ksp_monitor"] = None
+
+
+
+
+
+
+
stokes.petsc_options["snes_type"] = "newtonls"
+stokes.petsc_options["ksp_type"] = "fgmres"
+
+# stokes.petsc_options.setValue("fieldsplit_velocity_pc_type", "mg")
+stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_type", "kaskade")
+stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_cycle_type", "w")
+
+stokes.petsc_options["fieldsplit_velocity_mg_coarse_pc_type"] = "svd"
+stokes.petsc_options[f"fieldsplit_velocity_ksp_type"] = "fcg"
+stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_type"] = "chebyshev"
+stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_max_it"] = 7
+stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_converged_maxits"] = None
+
+# gasm is super-fast ... but mg seems to be bulletproof
+# gamg is toughest wrt viscosity
+
+stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "gamg")
+stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "additive")
+stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v")
+
+# # # mg, multiplicative - very robust ... similar to gamg, additive
+
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "mg")
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "multiplicative")
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v")
+
+
+
+
+
+
+
# stokes._setup_pointwise_functions(verbose=True)
+# stokes._setup_discretisation(verbose=True)
+# stokes.dm.ds.view()
+
+
+
+
+
+
+
# %%
+# Solve time
+stokes.solve()
+
+
+
+
+
+

Visualise it !#

+
+
+
# check the mesh if in a notebook / serial
+
+if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh)
+    pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v.sym)
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, stokes.bodyforce[1])
+    pvmesh.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh, v.sym.dot(v.sym))
+
+    velocity_points = vis.meshVariable_to_pv_cloud(v)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v.sym)
+
+    pl = pv.Plotter(window_size=(1000, 750))
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        scalars="Vmag",
+        use_transparency=False,
+        opacity=1.0,
+    )
+
+    arrows = pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=3.0, opacity=1, show_scalar_bar=False)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+

SolCx from the same setup#

+
+
+
stokes.bodyforce = sympy.Matrix(
+    [0, -sympy.cos(sympy.pi * x) * sympy.sin(2 * sympy.pi * y)]
+)
+
+viscosity_fn = sympy.Piecewise(
+    (1.0e6, x > x_c),
+    (1.0, True),
+)
+
+stokes.constitutive_model.Parameters.shear_viscosity_0 = viscosity_fn
+
+
+
+
+
+
+
stokes.constitutive_model.Parameters.shear_viscosity_0
+
+
+
+
+
+
+
stokes.saddle_preconditioner = sympy.simplify(1 / (stokes.constitutive_model.viscosity + stokes.penalty))
+
+
+
+
+
+
+
timing.reset()
+timing.start()
+stokes.solve(zero_init_guess=True)
+timing.print_table(display_fraction=0.999)
+
+# Save this solution
+
+with mesh.access(v0):
+    v0.data[...] = v.data[...]
+
+
+
+
+
+
+
# reset and re-do with natural bcs
+
+stokes._reset()
+stokes.tolerance = 1.0e-6
+stokes.add_natural_bc([0.0,1e6*v.sym[1]], "Top") 
+stokes.add_dirichlet_bc((sympy.oo,0.0), "Bottom")
+stokes.add_dirichlet_bc((0.0,sympy.oo), "Left")
+stokes.add_dirichlet_bc((0.0,sympy.oo), "Right")
+stokes.solve()
+
+with mesh.access(v1):
+    v1.data[...] = v.data[...]
+
+
+
+
+
+
+
# reset and re-do with natural bcs & petsc normals
+
+stokes._reset()
+stokes.tolerance = 1.0e-6
+
+Gamma = mesh.Gamma
+stokes.add_natural_bc(1e6 * Gamma.dot(v.sym) * Gamma, "Top") 
+stokes.add_dirichlet_bc((sympy.oo,0.0), "Bottom")
+stokes.add_dirichlet_bc((0.0,sympy.oo), "Left")
+stokes.add_dirichlet_bc((0.0,sympy.oo), "Right")
+stokes.solve()
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+
+if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh)
+    pvmesh.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh, v0.sym.dot(v0.sym))
+    pvmesh.point_data["Visc"] = vis.scalar_fn_to_pv_points(pvmesh, stokes.constitutive_model.Parameters.shear_viscosity_0)
+
+    pvmesh.point_data["V2"] = vis.vector_fn_to_pv_points(pvmesh, v.sym * stokes.constitutive_model.viscosity)
+    pvmesh.point_data["V0"] = vis.vector_fn_to_pv_points(pvmesh, v0.sym * stokes.constitutive_model.viscosity)
+    pvmesh.point_data["V1"] = vis.vector_fn_to_pv_points(pvmesh, v1.sym * stokes.constitutive_model.viscosity)
+    pvmesh.point_data["dV0"] = pvmesh.point_data["V1"] - pvmesh.point_data["V0"]
+    
+    velocity_points = vis.meshVariable_to_pv_cloud(v)
+    velocity_points.point_data["V2"] = vis.vector_fn_to_pv_points(velocity_points, v.sym)
+    velocity_points.point_data["V1"] = vis.vector_fn_to_pv_points(velocity_points, v1.sym)
+    velocity_points.point_data["V0"] = vis.vector_fn_to_pv_points(velocity_points, v0.sym)
+    velocity_points.point_data["dV1"] = velocity_points.point_data["V1"] - velocity_points.point_data["V0"]
+    velocity_points.point_data["dV2"] = velocity_points.point_data["V2"] - velocity_points.point_data["V0"]
+
+    pl = pv.Plotter(window_size=(1000, 750))
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        scalars="Vmag",
+        use_transparency=False,
+        opacity=1.0,
+    )
+
+    arrows0 = pl.add_arrows(velocity_points.points, velocity_points.point_data["V2"], mag=100.0, opacity=1, show_scalar_bar=False)
+    arrows1 = pl.add_arrows(velocity_points.points, velocity_points.point_data["dV2"], mag=100000.0, opacity=1, show_scalar_bar=False)
+
+    pl.show(jupyter_backend='client')
+
+
+
+
+
+
+
# %%
+try:
+    import underworld as uw2
+
+    solC = uw2.function.analytic.SolC()
+    vel_soln_analytic = solC.fn_velocity.evaluate(mesh.data)
+    from mpi4py import MPI
+
+    comm = MPI.COMM_WORLD
+    from numpy import linalg as LA
+
+    with mesh.access(v):
+        num = function.evaluate(v.fn, mesh.data)  # this appears busted
+        if comm.rank == 0:
+            print("Diff norm a. = {}".format(LA.norm(v.data - vel_soln_analytic)))
+            print("Diff norm b. = {}".format(LA.norm(num - vel_soln_analytic)))
+        # if not np.allclose(v.data, vel_soln_analytic, rtol=1):
+        #     raise RuntimeError("Solve did not produce expected result.")
+    comm.barrier()
+except ImportError:
+    import warnings
+
+    warnings.warn("Unable to test SolC results as UW2 not available.")
+
+# %%
+
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Cartesian_SolNL.html b/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Cartesian_SolNL.html new file mode 100644 index 0000000..25ee24c --- /dev/null +++ b/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Cartesian_SolNL.html @@ -0,0 +1,812 @@ + + + + + + + + + + + Non-linear Stokes benchmark — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Non-linear Stokes benchmark

+ +
+
+ +
+
+
+ + + + +
+ +
+

Non-linear Stokes benchmark#

+
+
+
# %%
+# To add a new cell, type '# %%'
+# To add a new markdown cell, type '# %% [markdown]'
+# %%
+from petsc4py import PETSc
+import underworld3 as uw
+from underworld3.systems import Stokes
+import numpy as np
+import sympy
+from mpi4py import MPI
+
+
+
+
+
+
+
rank = MPI.COMM_WORLD.rank
+
+
+
+
+
+
+
# options = PETSc.Options()
+# options["help"] = None
+
+
+
+
+
+
+
# options["ksp_rtol"] = 1.0e-8
+# options["snes_converged_reason"] = None
+# options["snes_monitor"] = None
+# # options["snes_monitor_short"] = None
+# # options["snes_view"]=None
+# options["snes_test_jacobian"] = None
+# options["snes_rtol"] = 1.0e-7
+# # options["snes_max_it"] = 1
+# # options["snes_linesearch_monitor"] = None
+
+
+
+
+
+
+
# %%
+n_els = 32
+mesh = uw.meshing.UnstructuredSimplexBox(
+    minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0), cellSize=1/n_els
+)
+
+
+
+
+
+
+
# %%
+# NL problem
+# Create solution functions
+from underworld3.function.analytic import (
+    AnalyticSolNL_velocity,
+    AnalyticSolNL_bodyforce,
+    AnalyticSolNL_viscosity,
+)
+
+
+
+
+
+
+
x, y = mesh.X
+
+
+
+
+
+
+
r = mesh.r
+eta0 = 1.0
+n = 1
+r0 = 1.5
+params = (eta0, n, r0)
+sol_bf_ijk = AnalyticSolNL_bodyforce(*params, *r)
+sol_vel_ijk = AnalyticSolNL_velocity(*params, *r)
+
+
+
+
+
+
+
sol_bf = mesh.vector.to_matrix(sol_bf_ijk)
+sol_vel = mesh.vector.to_matrix(sol_vel_ijk)
+sol_visc = AnalyticSolNL_viscosity(*params, *r)
+
+
+
+
+

debug - are problems just because there is no analytic solution module on mac +The solNL case is a MMS force term (complicated) designed to produce a specific +velocity field. This is a placeholder that just lets the non-linear problem run.

+
+
+
# sol_vel = sympy.Matrix([0, 0])
+# sol_bf = sympy.Matrix([0, sympy.cos(3 * sympy.pi * x) * sympy.cos(3 * sympy.pi * y)])
+# sol_visc = 1
+
+
+
+
+
+
+
# %%
+v = uw.discretisation.MeshVariable("U", mesh, mesh.dim, degree=1)
+p = uw.discretisation.MeshVariable("P", mesh, 1, degree=0)
+
+
+
+
+
+
+
stokes = uw.systems.Stokes(mesh, velocityField=v, pressureField=p)
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.shear_viscosity_0 = 1
+
+
+
+
+
+
+
# BC
+
+stokes.add_dirichlet_bc(sol_vel, "Top", [0, 1])  
+stokes.add_dirichlet_bc(sol_vel, "Bottom", [0, 1])
+stokes.add_dirichlet_bc(sol_vel, "Left", [0, 1])
+stokes.add_dirichlet_bc(sol_vel, "Right", [0, 1])
+
+
+
+
+
+
+
stokes.petsc_options["snes_converged_reason"] = None
+stokes.petsc_options["snes_monitor"] = None
+stokes.petsc_options["ksp_monitor"] = None
+stokes.petsc_options["snes_rtol"] = 1.0e-5
+
+
+
+
+
+
+
stokes._setup_pointwise_functions(verbose=True)
+stokes._setup_discretisation(verbose=True)
+stokes.dm.ds.view()
+
+
+
+
+
+
+
# %%
+# do linear first to get reasonable starting place
+stokes.bodyforce = sol_bf
+stokes.solve()
+# %%
+# get strainrate
+sr = stokes.strainrate
+# not sure if the following is needed as div_u should be zero
+# sr -= (stokes.div_u / mesh.dim) * sympy.eye(mesh.dim)
+# second invariant of strain rate
+
+inv2 = sr[0, 0] ** 2 + sr[0, 1] ** 2 + sr[1, 0] ** 2 + sr[1, 1] ** 2
+inv2 = 1 / 2 * inv2
+inv2 = sympy.sqrt(inv2)
+alpha_by_two = 2 / r0 - 2
+
+
+
+
+
+
+
viscosity = 2 * eta0 * inv2**alpha_by_two
+
+
+
+
+
+
+
stokes.constitutive_model.Parameters.shear_viscosity_0 = viscosity
+
+
+
+
+
+
+
stokes.penalty = 1.0
+stokes.solve(zero_init_guess=False)
+
+
+
+
+
+
+
sol_vel.T
+
+
+
+
+
+
+
# %%
+vdiff = stokes.u.sym - sol_vel.T
+vdiff_dot_vdiff = uw.maths.Integral(mesh, vdiff.dot(vdiff)).evaluate()
+v_dot_v = uw.maths.Integral(mesh, stokes.u.fn.dot(stokes.u.fn)).evaluate()
+
+
+
+
+
+
+
import math
+
+
+
+
+
+
+
rel_rms_diff = math.sqrt(vdiff_dot_vdiff / v_dot_v)
+if rank == 0:
+    print(f"RMS diff = {rel_rms_diff}")
+
+
+
+
+
+
+
if not np.allclose(rel_rms_diff, 0.00109, rtol=1.0e-2):
+    raise RuntimeError("Solve did not produce expected result.")
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Disk_CylCoords.html b/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Disk_CylCoords.html new file mode 100644 index 0000000..7bde910 --- /dev/null +++ b/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Disk_CylCoords.html @@ -0,0 +1,995 @@ + + + + + + + + + + + Cylindrical Stokes — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Cylindrical Stokes

+ +
+ +
+
+ + + + +
+ +
+

Cylindrical Stokes#

+

(In cylindrical coordinates)

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+
+import underworld3 as uw
+from underworld3 import timing
+import numpy as np
+import sympy
+
+from IPython.display import display
+import os
+
+os.environ["UW_TIMING_ENABLE"] = "1"
+
+
+
+
+
+
+
# Define the problem size
+#      1 - ultra low res for automatic checking
+#      2 - low res problem to play with this notebook
+#      3 - medium resolution (be prepared to wait)
+#      4 - highest resolution (benchmark case from Spiegelman et al)
+
+problem_size = 2
+
+# For testing and automatic generation of notebook output,
+# over-ride the problem size if the UW_TESTING_LEVEL is set
+
+uw_testing_level = os.environ.get("UW_TESTING_LEVEL")
+if uw_testing_level:
+    try:
+        problem_size = int(uw_testing_level)
+    except ValueError:
+        # Accept the default value
+        pass
+
+r_o = 1.0
+r_i = 0.5
+free_slip_upper = True
+
+if problem_size <= 1:
+    res = 0.1
+elif problem_size == 2:
+    res = 0.075
+elif problem_size == 3:
+    res = 0.05
+elif problem_size >= 4:
+    res = 0.01
+
+
+
+
+
+
+
meshball_xyz_tmp = uw.meshing.Annulus(
+    radiusOuter=r_o,
+    radiusInner=r_i,
+    cellSize=res,
+    refinement=0,
+    filename="tmp_meshball.msh",
+)
+
+
+
+
+
+
+
## We don't have the native coordinates built in to this mesh
+
+xy_vec = meshball_xyz_tmp.dm.getCoordinates()
+xy = xy_vec.array.reshape(-1, 2)
+
+dmplex = meshball_xyz_tmp.dm.clone()
+
+rtheta = np.empty_like(xy)
+rtheta[:, 0] = np.sqrt(xy[:, 0] ** 2 + xy[:, 1] ** 2)
+rtheta[:, 1] = np.arctan2(xy[:, 1] + 1.0e-16, xy[:, 0] + 1.0e-16)
+rtheta_vec = xy_vec.copy()
+rtheta_vec.array[...] = rtheta.reshape(-1)[...]
+dmplex.setCoordinates(rtheta_vec)
+# del meshball_xyz_tmp
+
+from enum import Enum
+class boundaries(Enum):
+    Lower = 1
+    Upper = 2
+    Centre = 10
+
+meshball = uw.meshing.Mesh(
+    dmplex,
+    boundaries = boundaries,
+    coordinate_system_type=uw.coordinates.CoordinateSystemType.CYLINDRICAL2D_NATIVE,
+    
+    qdegree=3,
+)
+uw.cython.petsc_discretisation.petsc_dm_set_periodicity(
+    meshball.dm, [0.0, 1.0], [0.0, 0.0], [0.0, 2 * np.pi]
+)
+meshball.dm.view()
+
+meshball_xyz = uw.meshing.Annulus(
+    radiusOuter=r_o, radiusInner=r_i, cellSize=res, qdegree=3
+)
+
+display(meshball_xyz.CoordinateSystem.type)
+display(meshball_xyz.CoordinateSystem.N)
+display(meshball_xyz.CoordinateSystem.R)
+display(meshball_xyz.CoordinateSystem.r)
+display(meshball_xyz.CoordinateSystem.X)
+display(meshball_xyz.CoordinateSystem.x)
+
+display(meshball.CoordinateSystem.type)
+display(meshball.CoordinateSystem.N)
+display(meshball.CoordinateSystem.R)
+display(meshball.CoordinateSystem.r)
+display(meshball.CoordinateSystem.X)
+display(meshball.CoordinateSystem.x)
+
+x, y = meshball.CoordinateSystem.X
+r, t = meshball.CoordinateSystem.R
+
+v_soln = uw.discretisation.MeshVariable("U", meshball, 2, degree=2)
+p_soln = uw.discretisation.MeshVariable("P", meshball, 1, degree=1, continuous=True)
+p_cont = uw.discretisation.MeshVariable("Pc", meshball, 1, degree=2)
+
+v_soln_xy = uw.discretisation.MeshVariable("Uxy", meshball_xyz, 2, degree=2)
+p_soln_xy = uw.discretisation.MeshVariable(
+    "Pxy", meshball_xyz, 1, degree=1, continuous=True
+)
+r_xy = uw.discretisation.MeshVariable("Rxy", meshball_xyz, 1, degree=1, continuous=True)
+
+
+
+
+
+
+
# Create a density structure / buoyancy force
+# gravity will vary linearly from zero at the centre
+# of the sphere to (say) 1 at the surface
+
+# Some useful coordinate stuff
+
+r, th = meshball.CoordinateSystem.R
+x, y = meshball.CoordinateSystem.X
+
+unit_rvec = meshball.CoordinateSystem.unit_e_0
+gravity_fn = r / r_o
+
+#
+Rayleigh = 1.0e5
+
+
+
+
+
+
+
# Create Stokes object (r, theta)
+
+stokes = uw.systems.Stokes(
+    meshball, velocityField=v_soln, pressureField=p_soln, solver_name="stokes"
+)
+
+stokes.tolerance = 1.0e-6
+stokes.petsc_options["snes_monitor"] = None
+
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.shear_viscosity_0 = 1
+
+# Velocity boundary conditions
+
+if not free_slip_upper:
+    stokes.add_dirichlet_bc(0.0, "Upper", 0)
+    stokes.add_dirichlet_bc(0.0, "Upper", 1)
+
+else:
+    stokes.add_dirichlet_bc(0.0, "Upper", 0)
+
+stokes.add_dirichlet_bc(0.0, "Lower", 0)
+# stokes.add_dirichlet_bc(0.0, "Lower", 1)
+
+
+
+
+
+
+
stokes.view()
+
+
+
+
+
+

Strain rate in Cylindrical (2D) geometry is this:#

+
+\[ \dot\epsilon_{rr} = \frac{\partial u_r}{\partial r}\]
+
+\[ \dot\epsilon_{\theta\theta} = \frac{1}{r} \frac{\partial u_\theta}{\partial \theta} + \frac{u_r}{r} \]
+
+\[ 2\dot\epsilon_{r\theta} = \frac{1}{r} \frac{\partial u_r}{\partial \theta} + \frac{\partial u_\theta}{\partial r} - \frac{u_\theta}{r} \]
+
+
+
meshball.vector.strain_tensor(stokes.Unknowns.u.sym)
+
+
+
+
+
+
+
# Create Stokes object (x,y)
+
+radius_fn = meshball_xyz.CoordinateSystem.xR[0]
+radius_fn = r_xy.sym[0]
+
+unit_rvec_xy = meshball_xyz.CoordinateSystem.unit_e_0
+
+stokes_xy = uw.systems.Stokes(
+    meshball_xyz,
+    velocityField=v_soln_xy,
+    pressureField=p_soln_xy,
+    solver_name="stokes_xy",
+)
+
+stokes_xy.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes_xy.constitutive_model.Parameters.shar_viscosity_0 = 1
+stokes_xy.petsc_options["snes_rtol"] = 1.0e-6
+stokes_xy.petsc_options["snes_monitor"] = None
+
+# Velocity boundary conditions
+
+if not free_slip_upper:
+    stokes_xy.add_dirichlet_bc(0.0, "Upper", 0)
+    stokes_xy.add_dirichlet_bc(0.0, "Upper", 1)
+else:
+    print("Free slip !")
+    penalty = 100000
+    stokes_xy.add_natural_bc(
+            penalty * unit_rvec_xy.dot(v_soln_xy.sym) * unit_rvec_xy, "Upper"
+        )
+    stokes_xy.add_natural_bc(
+            penalty * unit_rvec_xy.dot(v_soln_xy.sym) * unit_rvec_xy, "Lower"
+        )
+
+# stokes_xy.add_dirichlet_bc([0.0, 0.0], "Lower")
+
+
+
+
+
+
+
unit_rvec_xy
+
+
+
+
+
+
+
penalty * unit_rvec_xy.dot(v_soln_xy.sym) * unit_rvec_xy
+
+
+
+
+
+
+
# t_init = 10.0 * sympy.exp(-5.0 * (x**2 + (y - 0.5) ** 2))
+t_init = sympy.cos(4 * th)
+stokes.bodyforce = sympy.Matrix([Rayleigh * t_init, 0])
+
+# ----
+t_init_xy = sympy.cos(4 * meshball_xyz.CoordinateSystem.xR[1])
+unit_rvec = meshball_xyz.CoordinateSystem.unit_e_0
+stokes_xy.bodyforce = Rayleigh * t_init_xy * unit_rvec
+
+
+
+
+
+
+
stokes_xy.bodyforce
+
+
+
+
+
+
+
with meshball_xyz.access(r_xy):
+    r_xy.data[:, 0] = uw.function.evaluate(
+        meshball_xyz.CoordinateSystem.xR[0],
+        coords=r_xy.coords,
+        coord_sys=meshball_xyz.N,
+    )
+
+
+
+
+
+
+
timing.start()
+stokes.solve(zero_init_guess=True)
+timing.print_table()
+
+
+
+
+
+
+
timing.start()
+
+
+
+
+
+
+
stokes_xy.solve(zero_init_guess=True)
+
+
+
+
+
+
+
timing.print_table()
+
+
+
+
+
+
+
U_xy = meshball.CoordinateSystem.xRotN * v_soln.sym.T
+
+
+
+
+
+
+
# Visuals
+
+if uw.mpi.size == 1:
+    import underworld3.visualisation as vis  # use this module for plotting
+    import pyvista as pv
+    import vtk
+
+pl = pv.Plotter(window_size=(1000, 1000))
+
+pvmesh = uw.visualisation.mesh_to_pv_mesh(meshball_xyz)
+pvmesh.point_data["T"] = uw.visualisation.scalar_fn_to_pv_points(pvmesh, t_init_xy)
+
+velocity_points = uw.visualisation.meshVariable_to_pv_cloud(v_soln_xy)
+velocity_points_rt = uw.visualisation.meshVariable_to_pv_cloud(v_soln)
+
+velocity_points.point_data["Vxy"] = uw.visualisation.vector_fn_to_pv_points(
+    velocity_points, v_soln_xy.sym
+)
+velocity_points.point_data["Vrt"] = uw.visualisation.vector_fn_to_pv_points(
+    velocity_points_rt, U_xy.T
+)
+
+pl.add_mesh(
+    pvmesh,
+    cmap="coolwarm",
+    edge_color="Black",
+    show_edges=True,
+    scalars="T",
+    use_transparency=False,
+    opacity=1.0,
+)
+
+pl.add_arrows(
+    velocity_points.points,
+    velocity_points.point_data["Vxy"],
+    mag=1.0e-4,
+    opacity=0.75,
+    color="Black",
+)
+pl.add_arrows(
+    velocity_points.points,
+    velocity_points.point_data["Vrt"],
+    mag=1.0e-4,
+    opacity=0.75,
+    color="Green",
+)
+
+pl.camera.SetPosition(0.75, 0.2, 1.5)
+pl.camera.SetFocalPoint(0.75, 0.2, 0.0)
+pl.camera.SetClippingRange(1.0, 8.0)
+
+pl.show()
+
+
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Ellipse_Cartesian.html b/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Ellipse_Cartesian.html new file mode 100644 index 0000000..a3eda88 --- /dev/null +++ b/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Ellipse_Cartesian.html @@ -0,0 +1,973 @@ + + + + + + + + + + + Stokes (Cartesian formulation) in Elliptical Domain — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Stokes (Cartesian formulation) in Elliptical Domain

+ +
+ +
+
+ + + + +
+ +
+

Stokes (Cartesian formulation) in Elliptical Domain#

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+
+import underworld3 as uw
+from underworld3.systems import Stokes
+from underworld3 import function
+
+import numpy as np
+import sympy
+
+import os
+os.environ["UW_TIMING_ENABLE"] = "1"
+
+
+
+
+
+
+

+free_slip_upper = True
+free_slip_lower = True
+
+# Define the problem size
+#      1 - ultra low res for automatic checking
+#      2 - low res problem to play with this notebook
+#      3 - medium resolution (be prepared to wait)
+#      4 - highest resolution (benchmark case from Spiegelman et al)
+
+problem_size = 3
+
+# For testing and automatic generation of notebook output,
+# over-ride the problem size if the UW_TESTING_LEVEL is set
+
+uw_testing_level = os.environ.get("UW_TESTING_LEVEL")
+if uw_testing_level:
+    try:
+        problem_size = int(uw_testing_level)
+    except ValueError:
+        # Accept the default value
+        pass
+
+r_o = 1.0
+r_i = 0.5
+
+if problem_size <= 1:
+    res = 0.5
+elif problem_size == 2:
+    res = 0.1
+elif problem_size == 3:
+    res = 0.05
+elif problem_size == 4:
+    res = 0.025
+elif problem_size == 5:
+    res = 0.01
+elif problem_size >= 6:
+    res = 0.005
+
+
+cellSizeOuter = res
+cellSizeInner = res/2
+ellipticityOuter = 1.5
+ellipticityInner = 1.0
+
+radiusOuter = r_o
+radiusInner = r_i
+
+
+
+
+
+
+
from enum import Enum
+
+class boundaries(Enum):
+    Inner = 1
+    Outer = 2
+
+if uw.mpi.rank == 0:
+    import gmsh
+
+    gmsh.initialize()
+    gmsh.option.setNumber("General.Verbosity", 1)
+    gmsh.model.add("Annulus")
+
+    p0 = gmsh.model.geo.add_point(0.00, 0.00, 0.00, meshSize=cellSizeInner)
+
+    loops = []
+
+    p1 = gmsh.model.geo.add_point(radiusInner*ellipticityInner, 0.0, 0.0, meshSize=cellSizeInner)
+    p2 = gmsh.model.geo.add_point(0.0, radiusInner, 0.0, meshSize=cellSizeInner)
+    p3 = gmsh.model.geo.add_point(-radiusInner*ellipticityInner, 0.0, 0.0, meshSize=cellSizeInner)
+    p4 = gmsh.model.geo.add_point(0.0, -radiusInner, 0.0, meshSize=cellSizeInner)
+        
+    c1 = gmsh.model.geo.add_ellipse_arc(p1, p0, p1, p2)
+    c2 = gmsh.model.geo.add_ellipse_arc(p2, p0, p3, p3)
+    c3 = gmsh.model.geo.add_ellipse_arc(p3, p0, p3, p4)
+    c4 = gmsh.model.geo.add_ellipse_arc(p4, p0, p1, p1)
+
+    cl1 = gmsh.model.geo.add_curve_loop([c1, c2, c3, c4], tag=boundaries.Inner.value)
+
+    loops = [cl1] + loops
+
+    p5 = gmsh.model.geo.add_point(radiusOuter*ellipticityOuter, 0.0, 0.0, meshSize=cellSizeOuter)
+    p6 = gmsh.model.geo.add_point(0.0, radiusOuter, 0.0, meshSize=cellSizeOuter)
+    p7 = gmsh.model.geo.add_point(-radiusOuter*ellipticityOuter, 0.0, 0.0, meshSize=cellSizeOuter)
+    p8 = gmsh.model.geo.add_point(0.0, -radiusOuter, 0.0, meshSize=cellSizeOuter)
+        
+    c5 = gmsh.model.geo.add_ellipse_arc(p5, p0, p5, p6)
+    c6 = gmsh.model.geo.add_ellipse_arc(p6, p0, p7, p7)
+    c7 = gmsh.model.geo.add_ellipse_arc(p7, p0, p7, p8)
+    c8 = gmsh.model.geo.add_ellipse_arc(p8, p0, p5, p5)
+
+    # l1 = gmsh.model.geo.add_line(p5, p4)
+
+    cl2 = gmsh.model.geo.add_curve_loop([c5, c6, c7, c8], tag=boundaries.Outer.value)
+
+    loops = [cl2] + loops
+
+    s = gmsh.model.geo.add_plane_surface(loops)
+    gmsh.model.geo.synchronize()
+    gmsh.model.mesh.embed(0, [p0], 2, s)
+
+    gmsh.model.addPhysicalGroup(
+        1,
+        [c1, c2, c3, c4],
+        boundaries.Inner.value,
+        name=boundaries.Inner.name,
+    )
+
+    gmsh.model.addPhysicalGroup(
+        1, [c5, c6, c7, c8], 
+        boundaries.Outer.value, 
+        name=boundaries.Outer.name
+    )
+    gmsh.model.addPhysicalGroup(2, [s], 666666, "Elements")
+
+    gmsh.model.geo.synchronize()
+
+    gmsh.model.mesh.generate(2)
+    gmsh.write("tmp_elliptical_mesh.msh")
+    gmsh.finalize()
+
+elliptical_mesh = uw.discretisation.Mesh(
+        "tmp_elliptical_mesh.msh",
+        degree=1,
+        qdegree=3,
+        useMultipleTags=True,
+        useRegions=True,
+        markVertices=True,
+        boundaries=boundaries,
+        coordinate_system_type=None,
+        refinement=0,
+        refinement_callback=None,
+        return_coords_to_bounds=None,
+    )
+
+x,y = elliptical_mesh.X
+
+
+
+
+
+
+
elliptical_mesh.dm.view()
+
+
+
+
+
+
+
# Analytic expression for surface normals
+
+Gamma_N_Outer = sympy.Matrix([2 * x / ellipticityOuter**2, 2 * y ]).T
+Gamma_N_Outer = Gamma_N_Outer / sympy.sqrt(Gamma_N_Outer.dot(Gamma_N_Outer))
+Gamma_N_Inner = sympy.Matrix([2 * x / ellipticityInner**2, 2 * y ]).T
+Gamma_N_Inner = Gamma_N_Inner / sympy.sqrt(Gamma_N_Inner.dot(Gamma_N_Inner))
+
+
+
+
+
+
+
# Some geometry things
+
+x, y = elliptical_mesh.CoordinateSystem.X
+
+radius_fn = sympy.sqrt(x**2+y**2)
+unit_rvec = elliptical_mesh.CoordinateSystem.X / radius_fn
+
+# Some useful coordinate stuff
+
+
+
+
+
+
+
# Test that the second one is skipped
+
+v_soln = uw.discretisation.MeshVariable("U", elliptical_mesh, 2, degree=2, continuous=True, varsymbol=r"\mathbf{u}")
+v_soln_1 = uw.discretisation.MeshVariable("U1", elliptical_mesh, 2, degree=2, continuous=True, varsymbol=r"{\mathbf{u}^[1]}")
+v_soln_0 = uw.discretisation.MeshVariable("U0", elliptical_mesh, 2, degree=2, continuous=True, varsymbol=r"{\mathbf{u}^[0]}")
+p_soln = uw.discretisation.MeshVariable("P", elliptical_mesh, 1, degree=1, continuous=True, varsymbol=r"\mathbf{p}")
+
+
+
+
+
+
+
n_vect = uw.discretisation.MeshVariable("Gamma", elliptical_mesh, 2, degree=2, varsymbol="{\Gamma_N}")
+
+projection = uw.systems.Vector_Projection(elliptical_mesh, n_vect)
+projection.uw_function = sympy.Matrix([[0,0]])
+
+# r.dot(Gamma) Ensure consistent orientation (not needed for mesh boundary surfaces)
+
+GammaNorm = unit_rvec.dot(elliptical_mesh.Gamma) / sympy.sqrt(elliptical_mesh.Gamma.dot(elliptical_mesh.Gamma))
+
+projection.add_natural_bc(elliptical_mesh.Gamma * GammaNorm, "Outer")
+projection.add_natural_bc(elliptical_mesh.Gamma * GammaNorm, "Inner")
+
+projection.solve()
+
+# Ensure n_vect are unit vectors 
+with elliptical_mesh.access(n_vect):
+    n_vect.data[:,:] /= np.sqrt(n_vect.data[:,0]**2 + n_vect.data[:,1]**2).reshape(-1,1)
+
+
+
+
+
+
+
# Create Stokes object
+
+stokes = Stokes(elliptical_mesh, velocityField=v_soln, pressureField=p_soln, solver_name="stokes")
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.shear_viscosity_0 = 1
+stokes.penalty = 1.0
+stokes.saddle_preconditioner = sympy.simplify(1 / (stokes.constitutive_model.viscosity + stokes.penalty))
+
+stokes.petsc_options["fieldsplit_velocity_mg_coarse_pc_type"] = "svd"
+
+# Surface normals provided by DMPLEX
+
+Gamma = elliptical_mesh.Gamma
+stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) *  Gamma, "Outer")
+stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) *  Gamma, "Inner")
+
+
+
+
+
+
+
t_init = sympy.cos(4 * sympy.atan2(y,x))
+
+stokes.bodyforce = unit_rvec * t_init
+stokes.petsc_options["ksp_monitor"] = None
+stokes.petsc_options["snes_monitor"] = None
+stokes.tolerance = 1.0e-6
+
+
+
+
+
+
+
from underworld3 import timing
+
+timing.reset()
+timing.start()
+
+stokes.solve(zero_init_guess=True, debug=False)
+
+with elliptical_mesh.access(v_soln_1):
+    v_soln_1.data[...] = v_soln.data[...]
+
+timing.print_table()
+
+
+
+
+
+
+
# Create Stokes object
+
+stokes._reset()
+
+# Surface normals (computed)
+
+stokes.add_natural_bc(10000 * n_vect.sym.dot(v_soln.sym) * n_vect.sym, "Outer")
+stokes.add_natural_bc(10000 * n_vect.sym.dot(v_soln.sym) * n_vect.sym, "Inner")
+    
+
+stokes.solve(zero_init_guess=False)
+
+with elliptical_mesh.access(v_soln_0):
+    v_soln_0.data[...] = v_soln.data[...]
+
+
+
+
+
+
+
# Create Stokes object
+
+stokes._reset()
+
+# Surface normals (computed analytically)
+stokes.add_natural_bc(10000 * Gamma_N_Outer.dot(v_soln.sym) *  Gamma_N_Outer, "Outer")
+stokes.add_natural_bc(10000 * Gamma_N_Inner.dot(v_soln.sym) *  Gamma_N_Inner, "Inner")
+    
+stokes.solve(zero_init_guess=False)
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+
+if uw.mpi.size == 1:
+
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(elliptical_mesh)
+    velocity_points = vis.meshVariable_to_pv_cloud(v_soln)
+    velocity_points.point_data["Va"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym)
+    velocity_points.point_data["Vn"] = vis.vector_fn_to_pv_points(velocity_points, v_soln_1.sym)
+    velocity_points.point_data["Vp"] = vis.vector_fn_to_pv_points(velocity_points, v_soln_0.sym)
+    velocity_points.point_data["dVn"] = velocity_points.point_data["Vn"] - velocity_points.point_data["Va"]
+    velocity_points.point_data["dVp"] = velocity_points.point_data["Vp"] - velocity_points.point_data["Va"]
+
+    
+    pvmesh.point_data["Va"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)
+    pvmesh.point_data["Vn"] = vis.vector_fn_to_pv_points(pvmesh, v_soln_0.sym)
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_init)
+
+    points = np.zeros((elliptical_mesh._centroids.shape[0], 3))
+    points[:, 0] = elliptical_mesh._centroids[:, 0]
+    points[:, 1] = elliptical_mesh._centroids[:, 1]
+    point_cloud = pv.PolyData(points)
+
+    pvstream = pvmesh.streamlines_from_source(
+        point_cloud, vectors="Va", 
+        integration_direction="forward", 
+        integrator_type=2,
+        surface_streamlines=True,
+        initial_step_length=0.01,
+        max_time=1.0,
+        max_steps=2000
+    )
+    
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_mesh(
+                pvmesh,
+                cmap="coolwarm",
+                edge_color="Black",
+                scalars="T",
+                show_edges=True,
+                use_transparency=False,
+                opacity=0.75,
+               )
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["Va"], mag=3, color="Green")
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["Vn"], mag=3, color="Black")
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["Vp"], mag=3, color="Blue")
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["dVp"], mag=1000, color="Yellow")
+    
+    pl.add_mesh(pvstream)
+
+    pl.show(cpos="xy")
+
+
+
+
+

From the PETSc docs, the form of the boundary integral (residual, jacobian, preconditioner) and the form of the interior integrals

+
+

Neumann terms (boundary integrals)#

+

Boundary integral in mathematical form.

+
+\[\int_\Gamma \phi {\vec f}_0(u, u_t, \nabla u, x, t) \cdot \hat n + \nabla\phi \cdot {\overleftrightarrow f}_1(u, u_t, \nabla u, x, t) \cdot \hat n\]
+
+
+

Interior integrals#

+
+\[\int_\Omega \phi f_0(u, u_t, \nabla u, x, t) + \nabla\phi \cdot {\vec f}_1(u, u_t, \nabla u, x, t)\]
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Sinker.html b/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Sinker.html new file mode 100644 index 0000000..7c05fc0 --- /dev/null +++ b/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Sinker.html @@ -0,0 +1,1145 @@ + + + + + + + + + + + Multiple materials - Linear stokes sinker — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Multiple materials - Linear stokes sinker

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

Multiple materials - Linear stokes sinker#

+

This is the notorious “Stokes sinker” problem in which we have a dense and “rigid” (highly viscous) blob sinking in a low-viscosity fluid. This combination of high velocity and low strain rate is challenging for iterative solvers and there is a limit to the viscosity jujmp that can be introduced before the solvers fail to converge.

+

Sinker image with streamlines

+

We introduce the notion of an IndexSwarmVariable which automatically generates masks for a swarm +variable that consists of discrete level values (integers).

+

For a variable \(M\), the mask variables are \(\left\{ M^0, M^1 \ldots M^{N-1} \right\}\) where \(N\) is the number of indices (e.g. material types) on the variable. This value must be defined in advance.

+

The masks are orthogonal in the sense that \(M^i * M^j = 0\) if \(i \ne j\), and they are complete in the sense that \(\sum_i M^i = 1\) at all points.

+

The masks are implemented as continuous mesh variables (the user can specify the interpolation order) and so they are also differentiable (once).

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
# %%
+from petsc4py import PETSc
+import underworld3 as uw
+from underworld3.systems import Stokes
+import numpy as np
+import sympy
+from mpi4py import MPI
+
+import os
+
+os.environ["UW_TIMING_ENABLE"] = "1"
+
+if uw.mpi.size == 1:
+    os.makedirs("output", exist_ok=True)
+else:
+    os.makedirs(f"output_np{uw.mpi.size}", exist_ok=True)
+
+
+
+
+
+
+
uw
+
+
+
+
+
+
+
# Define the problem size
+#      1 - ultra low res for automatic checking
+#      2 - low res problem to play with this notebook
+#      3 - medium resolution (be prepared to wait)
+#      4 - highest resolution (benchmark case from Spiegelman et al)
+
+problem_size = 2
+
+# For testing and automatic generation of notebook output,
+# over-ride the problem size if the UW_TESTING_LEVEL is set
+
+uw_testing_level = os.environ.get("UW_TESTING_LEVEL")
+if uw_testing_level:
+    try:
+        problem_size = int(uw_testing_level)
+    except ValueError:
+        # Accept the default value
+        pass
+
+
+
+
+
+
+
sys = PETSc.Sys()
+sys.pushErrorHandler("traceback")
+
+
+
+
+
+
+
if problem_size <= 1:
+    res = 8
+elif problem_size == 2:
+    res = 16
+elif problem_size == 3:
+    res = 32
+elif problem_size == 4:
+    res = 48
+elif problem_size == 5:
+    res = 64
+elif problem_size >= 6:
+    res = 128
+
+
+
+
+
+
+
# Set size and position of dense sphere.
+sphereRadius = 0.1
+sphereCentre = (0.0, 0.7)
+
+
+
+
+
+
+
# define some names for our index
+materialLightIndex = 0
+materialHeavyIndex = 1
+
+
+
+
+
+
+
# Set constants for the viscosity and density of the sinker.
+viscBG = 1.0
+viscSphere = 1.0e6
+
+
+
+
+
+
+
expt_name = f"output/stinker_eta{viscSphere}_rho10_res{res}"
+
+
+
+
+
+
+
densityBG = 1.0
+densitySphere = 10.0
+
+
+
+
+
+
+
# location of tracer at bottom of sinker
+x_pos = sphereCentre[0]
+y_pos = sphereCentre[1] - sphereRadius
+
+
+
+
+
+
+
nsteps = 0
+
+
+
+
+
+
+
swarmGPC = 2
+
+
+
+
+
+
+
mesh = uw.meshing.UnstructuredSimplexBox(
+    minCoords=(-1.0, 0.0),
+    maxCoords=(1.0, 1.0),
+    cellSize=1.0 / res,
+    regular=False,
+    qdegree=3,
+)
+
+
+
+
+
+

Create Stokes object#

+
+
+
stokes = uw.systems.Stokes(mesh)
+
+v = stokes.Unknowns.u
+p = stokes.Unknowns.p
+
+# Set some options
+stokes.penalty = 1.0
+
+# Set some bcs
+stokes.add_dirichlet_bc((sympy.oo, 0.0), "Top")
+stokes.add_dirichlet_bc((sympy.oo, 0.0), "Bottom")
+stokes.add_dirichlet_bc((0.0,sympy.oo), "Left")
+stokes.add_dirichlet_bc((0.0,sympy.oo), "Right")
+
+
+
+
+
+
+
swarm = uw.swarm.Swarm(mesh=mesh)
+material = uw.swarm.IndexSwarmVariable(
+    "M", swarm, indices=2, proxy_continuous=False, proxy_degree=1
+)
+swarm.populate(fill_param=4)
+
+
+
+
+
+
+
blob = np.array([[sphereCentre[0], sphereCentre[1], sphereRadius, 1]])
+
+
+
+
+
+
+
with swarm.access(material):
+    material.data[...] = materialLightIndex
+
+    for i in range(blob.shape[0]):
+        cx, cy, r, m = blob[i, :]
+        inside = (swarm.data[:, 0] - cx) ** 2 + (swarm.data[:, 1] - cy) ** 2 < r**2
+        material.data[inside] = m
+
+
+
+
+
+
+
# %%
+tracer = np.zeros(shape=(1, 2))
+tracer[:, 0], tracer[:, 1] = x_pos, y_pos
+
+
+
+
+
+
+
density = densityBG * material.sym[0] + densitySphere * material.sym[1]
+viscosity = viscBG * material.sym[0] + viscSphere * material.sym[1]
+
+
+
+
+
+
+
# viscosity = sympy.Max( sympy.Min(viscosityMat, eta_max), eta_min)
+
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.shear_viscosity_0 = viscosity
+stokes.bodyforce = sympy.Matrix([0, -1 * density])
+
+
+
+
+
+
+
render = True
+
+
+
+
+
+
+
import pyvista as pv
+pl = pv.Plotter(notebook=True)
+pl.camera.position = (1.1, 1.5, 0.0)
+pl.camera.focal_point = (0.2, 0.3, 0.3)
+pl.camera.up = (0.0, 1.0, 0.0)
+pl.camera.zoom(1.4)
+
+def plot_T_mesh(filename):
+    if not render:
+        return
+
+    import numpy as np
+    import pyvista as pv
+    import underworld3.visualisation
+
+    pvmesh = uw.visualisation.mesh_to_pv_mesh(mesh)
+    point_cloud = underworld3.visualisation.swarm_to_pv_cloud(swarm)
+
+    with swarm.access():
+        point_cloud.point_data["M"] = material.data.copy()
+
+    ## Plotting into existing pl (memory leak in panel code)
+    pl.clear()
+
+    pl.add_mesh(pvmesh, "Black", "wireframe")
+
+    pl.add_points(
+        point_cloud,
+        cmap="coolwarm",
+        render_points_as_spheres=False,
+        point_size=10,
+        opacity=0.5,
+    )
+
+    pl.screenshot(
+        filename="{}.png".format(filename), window_size=(1280, 1280), return_img=False
+    )
+
+
+
+
+
+
+
# stokes.petsc_options.view()
+
+snes_rtol = 1.0e-6
+stokes.tolerance = snes_rtol
+
+# stokes.petsc_options["snes_converged_reason"] = None
+# stokes.petsc_options["ksp_type"] = "gmres"
+# stokes.petsc_options["ksp_rtol"] = 1.0e-9
+# stokes.petsc_options["ksp_atol"] = 1.0e-12
+# stokes.petsc_options["fieldsplit_pressure_ksp_rtol"] = 1.0e-8
+# stokes.petsc_options["fieldsplit_velocity_ksp_rtol"] = 1.0e-8
+# stokes.petsc_options["snes_atol"] = 0.1 * snes_rtol # by inspection
+stokes.petsc_options["ksp_monitor"] = None
+
+
+
+
+
+
+
nstep = 15
+
+
+
+
+
+
+
step = 0
+time = 0.0
+nprint = 0.0
+
+
+
+
+
+
+
# %%
+tSinker = np.zeros(nstep)
+ySinker = np.zeros(nstep)
+
+
+
+
+
+
+
from underworld3 import timing
+
+timing.reset()
+timing.start()
+stokes.solve(zero_init_guess=True)
+timing.print_table()
+
+
+
+
+
+
+
while step < nstep:
+    ### Get the position of the sinking ball
+    ymin = tracer[:, 1].min()
+    ySinker[step] = ymin
+    tSinker[step] = time
+
+    ### estimate dt
+    dt = stokes.estimate_dt()
+    if uw.mpi.rank == 0:
+        print(f"dt = {dt}", flush=True)
+
+    ## This way should be a bit safer in parallel where particles can move
+    ## processors in the middle of the calculation if you are not careful
+    ## PS - the function.evaluate needs fixing to take sympy.Matrix functions
+
+    swarm.advection(stokes.u.sym, dt, corrector=True)
+
+    ### solve stokes
+    stokes.solve(zero_init_guess=False)
+
+    ### print some stuff
+    if uw.mpi.size == 1:
+        print(f"Step: {str(step).rjust(3)}, time: {time:6.2f}, tracer:  {ymin:6.2f}")
+        plot_T_mesh(filename="{}_step_{}".format(expt_name, step))
+
+    mesh.write_timestep("stokesSinker", meshUpdates=False, meshVars=[p, v], index=step)
+
+    step += 1
+    time += dt
+
+
+
+
+
+
+
# %%
+if uw.mpi.rank == 0:
+    print("Initial position: t = {0:.3f}, y = {1:.3f}".format(tSinker[0], ySinker[0]))
+    print(
+        "Final position:   t = {0:.3f}, y = {1:.3f}".format(
+            tSinker[nsteps - 1], ySinker[nsteps - 1]
+        )
+    )
+
+    import matplotlib.pyplot as pyplot
+
+    fig = pyplot.figure()
+    fig.set_size_inches(12, 6)
+    ax = fig.add_subplot(1, 1, 1)
+    ax.plot(tSinker, ySinker)
+    ax.set_xlabel("Time")
+    ax.set_ylabel("Sinker position")
+
+
+
+
+
+
+
import numpy as np
+import pyvista as pv
+import underworld3 as uw
+import underworld3.visualisation
+
+pvmesh = uw.visualisation.mesh_to_pv_mesh(mesh)
+pvmesh.point_data["V"] = uw.visualisation.vector_fn_to_pv_points(pvmesh, v.sym)
+pvmesh.point_data["rho"] = uw.function.evaluate(density, mesh.data)
+
+swarm_points = underworld3.visualisation.swarm_to_pv_cloud(swarm)
+swarm_points.point_data["M"] = uw.visualisation.scalar_fn_to_pv_points(swarm_points, material.visMask())
+
+velocity_points = underworld3.visualisation.meshVariable_to_pv_cloud(v)
+velocity_points.point_data["X"] = uw.visualisation.coords_to_pv_coords(v.coords)
+velocity_points.point_data["V"] = uw.visualisation.vector_fn_to_pv_points(velocity_points, v.sym)
+
+
+
+
+
+
+

check if that worked#

+
+
+
if uw.mpi.size == 1:
+    
+    import numpy as np
+    import pyvista as pv
+    import underworld3 as uw
+    import underworld3.visualisation
+
+    pvmesh = uw.visualisation.mesh_to_pv_mesh(mesh)
+    pvmesh.point_data["V"] = uw.visualisation.vector_fn_to_pv_points(pvmesh, v.sym)
+    pvmesh.point_data["rho"] = uw.function.evaluate(density, mesh.data)
+
+    swarm_points = underworld3.visualisation.swarm_to_pv_cloud(swarm)
+    swarm_points.point_data["M"] = uw.visualisation.scalar_fn_to_pv_points(swarm_points, material.visMask())
+    
+    velocity_points = underworld3.visualisation.meshVariable_to_pv_cloud(v)
+    velocity_points.point_data["V"] = uw.visualisation.vector_fn_to_pv_points(velocity_points, v.sym)
+
+    pvstream = pvmesh.streamlines_from_source(
+        swarm_points,
+        vectors="V",
+        integration_direction="both",
+        max_steps=10,
+        surface_streamlines=True,
+        max_step_length=0.05,
+    )
+
+    pl = pv.Plotter(window_size=(1000, 750))
+
+    pl.add_mesh(pvmesh, "Black", "wireframe")
+
+    streamlines = pl.add_mesh(pvstream, opacity=0.25)
+    streamlines.SetVisibility(False)
+
+
+    pl.add_mesh(
+        swarm_points,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=False,
+        scalars="M",
+        use_transparency=False,
+        point_size=2.0,
+        opacity=0.5,
+        show_scalar_bar=False,
+    )
+
+    pl.add_mesh(
+        velocity_points,
+
+    )
+
+    arrows = pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=3.0, opacity=0.33, show_scalar_bar=False)
+
+
+    ## Widgets
+
+    def toggle_streamlines(flag):
+        streamlines.SetVisibility(flag)
+        
+    def toggle_arrows(flag):
+        arrows.SetVisibility(flag)
+
+    pl.add_checkbox_button_widget(toggle_streamlines, value=False, size = 10, position = (10, 20))
+    pl.add_checkbox_button_widget(toggle_arrows, value=False, size = 10, position = (30, 20))
+
+
+
+    # pl.screenshot(filename="SinkerSolution_hr.png", window_size=(4000, 2000))
+
+
+
+    
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
velocity_points.point_data["V"]
+
+
+
+
+
+
+
uw.function.evalf(v.sym[0], velocity_points.points[:,0:2])
+
+
+
+
+
+
+
velocity_points.point_data["V"]
+
+
+
+
+
+
+

+velocity_points.points.min()
+
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Spherical_Cap.html b/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Spherical_Cap.html new file mode 100644 index 0000000..94b69c3 --- /dev/null +++ b/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Spherical_Cap.html @@ -0,0 +1,936 @@ + + + + + + + + + + + Stokes flow in a Spherical Domain — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Stokes flow in a Spherical Domain

+ +
+ +
+
+ + + + +
+ +
+

Stokes flow in a Spherical Domain#

+
+

Mathematical formulation#

+

The Navier-Stokes equation describes the time-dependent flow of a viscous fluid in response to buoyancy forces and pressure gradients:

+
+\[ +\rho \frac{\partial \mathbf{u}}{\partial t} + \eta\nabla^2 \mathbf{u} -\nabla p = \rho \mathbf{g} +\]
+

Where \(\rho\) is the density, \(\eta\) is dynamic viscosity and \(\mathbf{g}\) is the gravitational acceleration vector. We here assume that density changes are due to temperature and are small enough to be consistent with an assumption of incompressibility (the Boussinesq approximation). We can rescale this equation of motion using units for length, time, temperature and mass that are specific to the problem and, in this way, obtain a scale-independent form:

+
+\[ +\frac{1}{\mathrm{Pr}} \frac{\partial \mathbf{u}}{\partial t} + \nabla^2 \mathbf{u} -\nabla p = \mathrm{Ra} T' \hat{\mathbf{g}} +\]
+

where we have assumed that buoyancy forces on the right hand side are due to temperature variations, and the two dimensionless numbers, \(\mathrm{Ra}\) and \(\mathrm{Pr}\) are measures of the importance of buoyancy forcing and intertial terms, respectively.

+
+\[ +\mathrm{Ra} = \frac{g\rho_0 \alpha \Delta T d^3}{\kappa \eta} +\quad \textrm{and} \quad +\mathrm{Pr} = \frac{\eta}{\rho \kappa} +\]
+

Here \(\alpha\) is the thermal expansivity, \(\Delta T\) is the range of the temperature variation, \(d\) is the typical length scale over which temperature varies, \(\kappa\) is the thermal diffusivity ( \(\kappa = k / \rho_0 C_p\); \(k\) is thermal conductivity, and \(C_p\) is heat capacity).

+

If we assume that the Prandtl number is large, then the inertial terms will not contribute significantly to the balance of forces in the equation of motion because we have rescaled the equations so that the velocity and pressure gradient terms are of order 1. This assumption eliminates the time dependent terms in the equations and tells us that the flow velocity and pressure field are always in equilibrium with the pattern of density variations and this also tells us that we can evaluate the flow without needing to know the history or origin of the buoyancy forces. When the viscosity is independent of velocity and dynamic pressure, the velocity and pressure scale proportionally with \(\mathrm{Ra}\) but the flow pattern itself is unchanged.

+

The scaling that we use for the non-dimensionalisation is as follows:

+
+\[ + x = d x', \quad t = \frac{d^2}{\kappa} t', \quad T=\Delta T T', \quad + p = p_0 + \frac{\eta \kappa}{d^2} p' +\]
+

where the stress (pressure) scaling using viscosity (\(\eta\)) determines how the mass scales. In the above, \(d\) is the radius of the inner core, a typical length scale for the problem, \(\Delta T\) is the order-of-magnitude range of the temperature variation from our observations, and \(\kappa\) is thermal diffusivity. The scaled velocity is obtained as \(v = \kappa / d v'\).

+
+
+

Formulation & model#

+

The model consists of a spherical ball divided into an unstructured tetrahedral mesh of quadratic velocity, linear pressure elements with a free slip upper boundary and with a buoyancy force pre-defined :

+
+\[ +T(r,\theta,\phi) = T_\textrm{TM}(\theta, \phi) \cdot r \sin(\pi r) +\]
+
+
+

Computational script in python#

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+import mpi4py
+import os
+
+os.environ["UW_TIMING_ENABLE"] = "1"
+
+import underworld3 as uw
+import numpy as np
+import sympy
+
+if uw.mpi.size == 1:
+    os.makedirs("output", exist_ok=True)
+else:
+    os.makedirs(f"output_np{uw.mpi.size}", exist_ok=True)
+
+
+
+
+
+
+
# Define the problem size
+#      1 - ultra low res for automatic checking
+#      2 - low res problem to play with this notebook
+#      3 - medium resolution (be prepared to wait)
+#      4 - highest resolution (benchmark case from Spiegelman et al)
+
+problem_size = uw.options.getInt("problem_size", default=4)
+grid_refinement = uw.options.getInt("grid_refinement", default=0)
+grid_simplex = uw.options.getBool("simplex", default=True)
+
+
+
+
+
+
+
visuals = 1
+output_dir = "output"
+
+# Some gmsh issues, so we'll use a pre-built one
+r_o = 1.0
+r_i = 0.547
+
+Rayleigh = 1.0e6  # Doesn't actually matter to the solution pattern,
+
+
+
+
+
+
+
if problem_size <= 1:
+    els = 3
+elif problem_size == 2:
+    els = 6
+elif problem_size == 3:
+    els = 12
+elif problem_size == 4:
+    els = 24
+elif problem_size == 5:  # Pretty extreme to mesh this on proc0
+    els = 48
+elif problem_size >= 6:  # should consider refinement (or prebuild)
+    els = 96
+
+cell_size = 1/els
+res = cell_size
+
+expt_name = f"Stokes_Spherical_Cap_free_slip_{els}"
+
+from underworld3 import timing
+
+timing.reset()
+timing.start()
+
+
+
+
+
+
+

+meshball = uw.meshing.RegionalSphericalBox(
+        radiusInner=r_i,
+        radiusOuter=r_o,
+        numElements=els,
+        refinement=grid_refinement,
+        qdegree=2,
+        simplex=grid_simplex,
+    )
+
+meshball.dm.view()
+
+
+
+
+
+
+
stokes = uw.systems.Stokes(
+    meshball,
+    verbose=False,
+    solver_name="stokes",
+)
+
+
+
+
+
+
+
v_soln = stokes.Unknowns.u
+p_soln = stokes.Unknowns.p
+
+
+
+
+
+
+
stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.viscosity = 1
+stokes.penalty = 1.0
+
+
+
+
+
+
+
# Create a density structure / buoyancy force
+# gravity will vary linearly from zero at the centre
+# of the sphere to (say) 1 at the surface
+
+# Some useful coordinate stuff
+
+x, y, z = meshball.CoordinateSystem.N
+ra, l1, l2 = meshball.CoordinateSystem.R
+
+## Mesh Variables for T ## 
+
+radius_fn = sympy.sqrt(
+    meshball.rvec.dot(meshball.rvec)
+)  # normalise by outer radius if not 1.0
+
+unit_rvec = meshball.X / (radius_fn)
+gravity_fn = radius_fn
+
+## Buoyancy (T) field
+
+t_soln = uw.discretisation.MeshVariable(r"\Delta T", meshball, 1, degree=2)
+
+t_forcing_fn = 1.0 * (
+    sympy.exp(-10.0 * (x**2 + (y - 0.8) ** 2 + z**2))
+    + sympy.exp(-10.0 * ((x - 0.8) ** 2 + y**2 + z**2))
+    + sympy.exp(-10.0 * (x**2 + y**2 + (z + 0.8) ** 2))
+)
+
+with meshball.access(t_soln):
+    t_soln.data[...] = uw.function.evaluate(
+        t_forcing_fn, t_soln.coords, meshball.N
+    ).reshape(-1, 1)
+
+
+
+
+
+
+
# Stokes settings
+
+stokes.tolerance = 1.0e-3
+stokes.petsc_options["ksp_monitor"] = None
+
+# stokes.petsc_options["ksp_type"] = "fgmres"
+
+# # stokes.petsc_options.setValue("fieldsplit_velocity_pc_type", "mg")
+# stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_type", "kaskade")
+# stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_cycle_type", "w")
+
+# stokes.petsc_options["fieldsplit_velocity_mg_coarse_pc_type"] = "svd"
+
+# stokes.petsc_options[f"fieldsplit_velocity_ksp_type"] = "fcg"
+# stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_type"] = "chebyshev"
+# stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_max_it"] = 5
+# stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_converged_maxits"] = None
+
+# # gasm is super-fast ... but mg seems to be bulletproof
+# # gamg is toughest wrt viscosity
+
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "gamg")
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "additive")
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v")
+
+# # # mg, multiplicative - very robust ... similar to gamg, additive
+
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "mg")
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "multiplicative")
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v")
+# thermal buoyancy force
+
+Gamma = meshball.Gamma
+stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) *  Gamma, "Upper")
+stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) *  Gamma, "Lower")
+stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) *  Gamma, "North")
+stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) *  Gamma, "East")
+stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) *  Gamma, "South")
+stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) *  Gamma, "West")
+
+stokes.bodyforce = unit_rvec * Rayleigh * gravity_fn * t_forcing_fn 
+
+
+
+
+
+
+
timing.reset()
+timing.start()
+
+stokes.solve(zero_init_guess=True)
+
+
+
+
+
+
+
timing.print_table()
+
+
+
+
+
+
+
outdir="output"
+
+meshball.write_timestep(
+    expt_name,
+    meshUpdates=True,
+    meshVars=[p_soln, v_soln],
+    outputPath=outdir,
+    index=0,
+)
+
+
+
+
+
+
+
# OR
+# check the mesh if in a notebook / serial
+
+if uw.mpi.size == 1:
+
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshball)
+    pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+    pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym)
+
+    velocity_points = vis.meshVariable_to_pv_cloud(v_soln)    
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym)
+
+    clipped = pvmesh.clip(origin=(0.0, 0.0, 0.0), normal=(0.0, 1, 0), invert=True)
+
+    points = np.zeros((meshball._centroids.shape[0], 3))
+    points[:, 0] = meshball._centroids[:, 0]
+    points[:, 1] = meshball._centroids[:, 1]
+    points[:, 2] = meshball._centroids[:, 2]
+
+    point_cloud = pv.PolyData(points)
+
+    pvstream = pvmesh.streamlines_from_source(
+        point_cloud, vectors="V", 
+        integration_direction="forward", 
+        integrator_type=45,
+        surface_streamlines=False,
+        initial_step_length=0.01,
+        max_time=0.25,
+        max_steps=1000
+    )
+
+    pl = pv.Plotter(window_size=[1000, 750])
+    pl.add_axes()
+
+    pl.add_mesh(
+        clipped,
+        # cmap="RdGy_r",
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=False,
+        scalars="T",
+        use_transparency=False,
+        show_scalar_bar = False,
+        opacity=1,
+    )
+
+    # pl.add_mesh(pvmesh, cmap="coolwarm", edge_color="Black", show_edges=True, scalars="T",
+    #               use_transparency=False, opacity=1.0)
+
+    
+    pl.add_mesh(pvstream)
+
+    arrows = pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], 
+                           show_scalar_bar = False, 
+                           mag=10/Rayleigh, )
+
+    # pl.screenshot(filename="sphere.png", window_size=(1000, 1000), return_img=False)
+    # OR
+    pl.show(cpos="xy")
+
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Spherical_FreeSlipBCs.html b/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Spherical_FreeSlipBCs.html new file mode 100644 index 0000000..6648d5e --- /dev/null +++ b/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Spherical_FreeSlipBCs.html @@ -0,0 +1,1079 @@ + + + + + + + + + + + Stokes flow in a Spherical Domain — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Stokes flow in a Spherical Domain

+ +
+ +
+
+ + + + +
+ +
+

Stokes flow in a Spherical Domain#

+
+

Mathematical formulation#

+

The Navier-Stokes equation describes the time-dependent flow of a viscous fluid in response to buoyancy forces and pressure gradients:

+
+\[ +\rho \frac{\partial \mathbf{u}}{\partial t} + \eta\nabla^2 \mathbf{u} -\nabla p = \rho \mathbf{g} +\]
+

Where \(\rho\) is the density, \(\eta\) is dynamic viscosity and \(\mathbf{g}\) is the gravitational acceleration vector. We here assume that density changes are due to temperature and are small enough to be consistent with an assumption of incompressibility (the Boussinesq approximation). We can rescale this equation of motion using units for length, time, temperature and mass that are specific to the problem and, in this way, obtain a scale-independent form:

+
+\[ +\frac{1}{\mathrm{Pr}} \frac{\partial \mathbf{u}}{\partial t} + \nabla^2 \mathbf{u} -\nabla p = \mathrm{Ra} T' \hat{\mathbf{g}} +\]
+

where we have assumed that buoyancy forces on the right hand side are due to temperature variations, and the two dimensionless numbers, \(\mathrm{Ra}\) and \(\mathrm{Pr}\) are measures of the importance of buoyancy forcing and intertial terms, respectively.

+
+\[ +\mathrm{Ra} = \frac{g\rho_0 \alpha \Delta T d^3}{\kappa \eta} +\quad \textrm{and} \quad +\mathrm{Pr} = \frac{\eta}{\rho \kappa} +\]
+

Here \(\alpha\) is the thermal expansivity, \(\Delta T\) is the range of the temperature variation, \(d\) is the typical length scale over which temperature varies, \(\kappa\) is the thermal diffusivity ( \(\kappa = k / \rho_0 C_p\); \(k\) is thermal conductivity, and \(C_p\) is heat capacity).

+

If we assume that the Prandtl number is large, then the inertial terms will not contribute significantly to the balance of forces in the equation of motion because we have rescaled the equations so that the velocity and pressure gradient terms are of order 1. This assumption eliminates the time dependent terms in the equations and tells us that the flow velocity and pressure field are always in equilibrium with the pattern of density variations and this also tells us that we can evaluate the flow without needing to know the history or origin of the buoyancy forces. When the viscosity is independent of velocity and dynamic pressure, the velocity and pressure scale proportionally with \(\mathrm{Ra}\) but the flow pattern itself is unchanged.

+

The scaling that we use for the non-dimensionalisation is as follows:

+
+\[ + x = d x', \quad t = \frac{d^2}{\kappa} t', \quad T=\Delta T T', \quad + p = p_0 + \frac{\eta \kappa}{d^2} p' +\]
+

where the stress (pressure) scaling using viscosity (\(\eta\)) determines how the mass scales. In the above, \(d\) is the radius of the inner core, a typical length scale for the problem, \(\Delta T\) is the order-of-magnitude range of the temperature variation from our observations, and \(\kappa\) is thermal diffusivity. The scaled velocity is obtained as \(v = \kappa / d v'\).

+
+
+

Formulation & model#

+

The model consists of a spherical ball divided into an unstructured tetrahedral mesh of quadratic velocity, linear pressure elements with a free slip upper boundary and with a buoyancy force pre-defined :

+
+\[ +T(r,\theta,\phi) = T_\textrm{TM}(\theta, \phi) \cdot r \sin(\pi r) +\]
+
+
+

Computational script in python#

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+import mpi4py
+import os
+
+os.environ["UW_TIMING_ENABLE"] = "1"
+
+import underworld3 as uw
+import numpy as np
+import sympy
+
+if uw.mpi.size == 1:
+    os.makedirs("output", exist_ok=True)
+else:
+    os.makedirs(f"output_np{uw.mpi.size}", exist_ok=True)
+
+
+
+
+
+
+
# Define the problem size
+#      1 - ultra low res for automatic checking
+#      2 - low res problem to play with this notebook
+#      3 - medium resolution (be prepared to wait)
+#      4 - highest resolution (benchmark case from Spiegelman et al)
+
+problem_size = uw.options.getInt("problem_size", default=3)
+grid_refinement = uw.options.getInt("grid_refinement", default=0)
+grid_type = uw.options.getString("grid_type", default="simplex")
+
+
+
+
+
+
+
visuals = 1
+output_dir = "output"
+grid_type = "simplex_sphere"
+
+# Some gmsh issues, so we'll use a pre-built one
+r_o = 1.0
+r_i = 0.547
+
+Rayleigh = 1.0e6  # Doesn't actually matter to the solution pattern,
+
+
+
+
+
+
+
if problem_size <= 1:
+    els = 3
+elif problem_size == 2:
+    els = 6
+elif problem_size == 3:
+    els = 12
+elif problem_size == 4:
+    els = 50
+    cell_size = 0.02
+elif problem_size == 5:  # Pretty extreme to mesh this on proc0
+    els = 66
+elif problem_size >= 6:  # should consider refinement (or prebuild)
+    els = 100
+
+cell_size = 1/els
+res = cell_size
+
+expt_name = f"Stokes_Sphere_free_slip_{els}"
+
+from underworld3 import timing
+
+timing.reset()
+timing.start()
+
+
+
+
+
+
+
if "ball" in grid_type:
+    meshball = uw.meshing.SegmentedSphericalBall(
+        radius=r_o,
+        cellSize=cell_size,
+        numSegments=5,
+        qdegree=2,
+        refinement=grid_refinement,
+    )
+elif "cubed" in grid_type:
+    meshball = uw.meshing.CubedSphere(
+        radiusInner=r_i,
+        radiusOuter=r_o,
+        numElements=els,
+        refinement=grid_refinement,
+        qdegree=2,
+    )
+else:
+    meshball = uw.meshing.SegmentedSphericalShell(
+        radiusInner=r_i,
+        radiusOuter=r_o,
+        cellSize=cell_size,
+        numSegments=5,
+        qdegree=2,
+        refinement=grid_refinement,
+    )
+
+meshball.dm.view()
+
+
+
+
+
+
+
if uw.mpi.size == 1:
+
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshball)
+
+    clipped = pvmesh.clip(origin=(0.0, 0.0, 0.0), normal=(0.0, 0.0, 1.0), invert=True, crinkle=True)
+
+    pl = pv.Plotter(window_size=[1000, 1000])
+    pl.add_axes()
+
+    pl.add_mesh(
+        clipped,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        use_transparency=False,
+        show_scalar_bar = False,
+        opacity=1.0,
+    )
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
stokes = uw.systems.Stokes(
+    meshball,
+    verbose=False,
+    solver_name="stokes",
+)
+
+
+
+
+
+
+
v_soln = stokes.Unknowns.u
+p_soln = stokes.Unknowns.p
+
+
+
+
+
+
+
stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.viscosity = 1
+stokes.penalty = 0.0
+
+
+
+
+
+
+
# Create a density structure / buoyancy force
+# gravity will vary linearly from zero at the centre
+# of the sphere to (say) 1 at the surface
+
+# Some useful coordinate stuff
+
+x, y, z = meshball.CoordinateSystem.N
+ra, l1, l2 = meshball.CoordinateSystem.R
+
+## Mesh Variables for T ## 
+
+radius_fn = sympy.sqrt(
+    meshball.rvec.dot(meshball.rvec)
+)  # normalise by outer radius if not 1.0
+
+unit_rvec = meshball.X / (radius_fn)
+gravity_fn = radius_fn
+
+## Buoyancy (T) field
+
+t_soln = uw.discretisation.MeshVariable(r"\Delta T", meshball, 1, degree=2)
+
+t_forcing_fn = 1.0 * (
+    sympy.exp(-10.0 * (x**2 + (y - 0.8) ** 2 + z**2))
+    + sympy.exp(-10.0 * ((x - 0.8) ** 2 + y**2 + z**2))
+    + sympy.exp(-10.0 * (x**2 + y**2 + (z - 0.8) ** 2))
+)
+
+with meshball.access(t_soln):
+    t_soln.data[...] = uw.function.evaluate(
+        t_forcing_fn, t_soln.coords, meshball.N
+    ).reshape(-1, 1)
+
+
+
+
+
+
+
# Rigid body rotations that are null-spaces for this set of bc's
+
+# We can remove these after the fact, but also useful to double check
+# that we are not adding anything to excite these modes in the forcing terms.
+
+orientation_wrt_z = sympy.atan2(y + 1.0e-10, x + 1.0e-10)
+v_rbm_z_x = -ra * sympy.sin(orientation_wrt_z)
+v_rbm_z_y = ra * sympy.cos(orientation_wrt_z)
+v_rbm_z = sympy.Matrix([v_rbm_z_x, v_rbm_z_y, 0]).T
+
+orientation_wrt_x = sympy.atan2(z + 1.0e-10, y + 1.0e-10)
+v_rbm_x_y = -ra * sympy.sin(orientation_wrt_x)
+v_rbm_x_z = ra * sympy.cos(orientation_wrt_x)
+v_rbm_x = sympy.Matrix([0, v_rbm_x_y, v_rbm_x_z]).T
+
+orientation_wrt_y = sympy.atan2(z + 1.0e-10, x + 1.0e-10)
+v_rbm_y_x = -ra * sympy.sin(orientation_wrt_y)
+v_rbm_y_z = ra * sympy.cos(orientation_wrt_y)
+v_rbm_y = sympy.Matrix([v_rbm_y_x, 0, v_rbm_y_z]).T
+
+
+
+
+
+
+
# Stokes settings
+
+stokes.tolerance = 1.0e-3
+stokes.petsc_options["ksp_monitor"] = None
+
+stokes.petsc_options["snes_type"] = "newtonls"
+stokes.petsc_options["ksp_type"] = "fgmres"
+
+# stokes.petsc_options.setValue("fieldsplit_velocity_pc_type", "mg")
+stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_type", "kaskade")
+stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_cycle_type", "w")
+
+stokes.petsc_options["fieldsplit_velocity_mg_coarse_pc_type"] = "svd"
+
+stokes.petsc_options[f"fieldsplit_velocity_ksp_type"] = "fcg"
+stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_type"] = "chebyshev"
+stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_max_it"] = 5
+stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_converged_maxits"] = None
+
+# gasm is super-fast ... but mg seems to be bulletproof
+# gamg is toughest wrt viscosity
+
+stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "gamg")
+stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "additive")
+stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v")
+
+# # # mg, multiplicative - very robust ... similar to gamg, additive
+
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "mg")
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "multiplicative")
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v")
+# thermal buoyancy force
+
+Gamma = meshball.CoordinateSystem.unit_e_0
+stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) *  Gamma, "UpperPlus")
+
+if not "ball" in grid_type:
+    stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) *  Gamma, "LowerPlus")
+
+stokes.bodyforce = unit_rvec * Rayleigh * gravity_fn * t_forcing_fn 
+
+
+
+
+
+
+
timing.reset()
+timing.start()
+
+stokes.solve(zero_init_guess=True)
+
+
+
+
+
+
+

+# Note: we should remove the rigid body rotation nullspace
+# This should be done during the solve, but it is also reasonable to
+# remove it from the force terms and solution to prevent it growing if present
+
+I0 = uw.maths.Integral(meshball, v_rbm_y.dot(v_rbm_y))
+norm = I0.evaluate()
+I0.fn = v_soln.sym.dot(v_soln.sym)
+vnorm = np.sqrt(I0.evaluate())
+
+# for i in range(10):
+
+I0.fn = v_soln.sym.dot(v_rbm_x)
+x_ns = I0.evaluate() / norm
+I0.fn = v_soln.sym.dot(v_rbm_y)
+y_ns = I0.evaluate() / norm
+I0.fn = v_soln.sym.dot(v_rbm_z)
+z_ns = I0.evaluate() / norm
+
+null_space_err = np.sqrt(x_ns**2 + y_ns**2 + z_ns**2) / vnorm
+
+print(
+    "Rigid body: {:.4}, {:.4}, {:.4} / {:.4}  (x,y,z axis / total)".format(
+        x_ns, y_ns, z_ns, null_space_err
+    )
+)
+
+
+
+
+
+
+
timing.print_table()
+
+
+
+
+
+
+
outdir="output"
+
+meshball.write_timestep(
+    expt_name,
+    meshUpdates=True,
+    meshVars=[p_soln, v_soln],
+    outputPath=outdir,
+    index=0,
+)
+
+
+
+
+
+
+
# OR
+# check the mesh if in a notebook / serial
+
+if uw.mpi.size == 1:
+
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshball)
+    pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym)
+    pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym)
+
+    velocity_points = vis.meshVariable_to_pv_cloud(v_soln)    
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym)
+
+    clipped = pvmesh.clip(origin=(0.0, 0.0, 0.0), normal=(0.0, 1, 0), invert=True)
+    clipped.point_data["V"] = vis.vector_fn_to_pv_points(clipped, v_soln.sym)
+    
+    clippedv = velocity_points.clip(origin=(0.0, 0.0, 0.0), normal=(0.0, 1, 0), invert=True)
+    clippedv.point_data["V"] = vis.vector_fn_to_pv_points(clippedv, v_soln.sym)
+
+    skip = 7
+    points = np.zeros((meshball._centroids[::skip].shape[0], 3))
+    points[:, 0] = meshball._centroids[::skip, 0]
+    points[:, 1] = meshball._centroids[::skip, 1]
+    points[:, 2] = meshball._centroids[::skip, 2]
+    point_cloud = pv.PolyData(points)
+
+    pvstream = pvmesh.streamlines_from_source(
+        point_cloud, vectors="V", 
+        integration_direction="forward", 
+        integrator_type=45,
+        surface_streamlines=False,
+        initial_step_length=0.01,
+        max_time=1.0,
+        max_steps=1000
+    )
+
+    pl = pv.Plotter(window_size=[1000, 750])
+    pl.add_axes()
+
+    pl.add_mesh(
+        clipped,
+        cmap="Reds",
+        # cmap="coolwarm",
+        edge_color="Black",
+        show_edges=False,
+        scalars="T",
+        use_transparency=False,
+        show_scalar_bar = False,
+        opacity=1,
+    )
+
+    # pl.add_mesh(pvmesh, cmap="coolwarm", edge_color="Black", show_edges=True, scalars="T",
+    #               use_transparency=False, opacity=1.0)
+
+    
+    pl.add_mesh(pvstream)
+
+    # arrows = pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], 
+    #                        show_scalar_bar = False, 
+    #                        mag=50/Rayleigh, )    
+    
+    arrows = pl.add_arrows(clippedv.points, clippedv.point_data["V"], 
+                           show_scalar_bar = False, 
+                           mag=100/Rayleigh, )
+
+    # pl.screenshot(filename="sphere.png", window_size=(1000, 1000), return_img=False)
+    # OR
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
pl.screenshot("snapshot.png", window_size=(2000,2000), return_img=False)
+
+
+
+
+
+
+
! open snapshot.png
+
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Swarm_Bubbles.html b/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Swarm_Bubbles.html new file mode 100644 index 0000000..98cb8d4 --- /dev/null +++ b/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Swarm_Bubbles.html @@ -0,0 +1,1014 @@ + + + + + + + + + + + Multiple materials - Drips and Blobs — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Multiple materials - Drips and Blobs

+ +
+
+ +
+
+
+ + + + +
+ +
+

Multiple materials - Drips and Blobs#

+

We introduce the notion of an IndexSwarmVariable which automatically generates masks for a swarm +variable that consists of discrete level values (integers).

+

For a variable \(M\), the mask variables are \(\left\{ M^0, M^1, M^2 \ldots M^{N-1} \right\}\) where \(N\) is the number of indices (e.g. material types) on the variable. This value must be defined in advance.

+

The masks are orthogonal in the sense that \(M^i * M^j = 0\) if \(i \ne j\), and they are complete in the sense that \(\sum_i M^i = 1\) at all points.

+

The masks are implemented as continuous mesh variables (the user can specify the interpolation order) and so they are also differentiable (once).

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+
+import underworld3 as uw
+from underworld3.systems import Stokes
+from underworld3 import function
+
+import numpy as np
+import sympy
+
+render = True
+
+
+
+
+
+
+
meshbox = uw.meshing.UnstructuredSimplexBox(
+    minCoords=(0.0, 0.0),
+    maxCoords=(1.0, 1.0),
+    cellSize=1.0 / 24.0,
+    regular=True,
+    qdegree=2,
+)
+
+
+
+
+
+
+
# meshbox.quadrature.view()
+
+
+
+
+
+
+
meshbox.dm.view()
+
+
+
+
+
+
+
# Some useful coordinate stuff
+
+x, y = meshbox.CoordinateSystem.X
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable("U", meshbox, meshbox.dim, degree=2)
+p_soln = uw.discretisation.MeshVariable("P", meshbox, 1, degree=1)
+
+
+
+
+
+
+
swarm = uw.swarm.Swarm(mesh=meshbox)
+material = uw.swarm.IndexSwarmVariable("M", swarm, indices=4, proxy_degree=1)
+swarm.populate(fill_param=4)
+
+
+
+
+
+
+
blobs = np.array(
+    [
+        [0.25, 0.75, 0.1, 1],
+        [0.45, 0.70, 0.05, 2],
+        [0.65, 0.60, 0.06, 3],
+        [0.85, 0.40, 0.06, 1],
+        [0.65, 0.20, 0.06, 2],
+        [0.45, 0.20, 0.12, 3],
+    ]
+)
+
+
+with swarm.access(material):
+    material.data[...] = 0
+
+    for i in range(blobs.shape[0]):
+        cx, cy, r, m = blobs[i, :]
+        inside = (swarm.data[:, 0] - cx) ** 2 + (swarm.data[:, 1] - cy) ** 2 < r**2
+        material.data[inside] = m
+
+
+
+
+
+
+
material.sym
+
+
+
+
+
+
+
X = meshbox.CoordinateSystem.X
+
+
+
+
+
+
+
# The material fields are differentiable
+
+sympy.derive_by_array(material.sym, X).reshape(2, 4).tomatrix()
+
+
+
+
+
+
+
mat_density = np.array([1, 0.1, 0.1, 2])
+density = (
+    mat_density[0] * material.sym[0]
+    + mat_density[1] * material.sym[1]
+    + mat_density[2] * material.sym[2]
+    + mat_density[3] * material.sym[3]
+)
+
+
+
+
+
+
+
mat_viscosity = np.array([1, 0.1, 10.0, 10.0])
+# mat_viscosity = np.array([1, 1, 1.0, 1.0])
+
+viscosity = (
+    mat_viscosity[0] * material.sym[0]
+    + mat_viscosity[1] * material.sym[1]
+    + mat_viscosity[2] * material.sym[2]
+    + mat_viscosity[3] * material.sym[3]
+)
+
+
+
+
+
+
+
if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshbox)
+    points = vis.swarm_to_pv_cloud(swarm)
+    point_cloud = pv.PolyData(points)
+    
+    pvmesh.point_data["M0"] = vis.scalar_fn_to_pv_points(pvmesh, material.sym[0])
+    pvmesh.point_data["M1"] = vis.scalar_fn_to_pv_points(pvmesh, material.sym[1])
+    pvmesh.point_data["M2"] = vis.scalar_fn_to_pv_points(pvmesh, material.sym[2])
+    pvmesh.point_data["M3"] = vis.scalar_fn_to_pv_points(pvmesh, material.sym[3])
+    pvmesh.point_data["M"] = (1.0 * pvmesh.point_data["M1"] 
+                              + 2.0 * pvmesh.point_data["M2"]
+                              + 3.0 * pvmesh.point_data["M3"])
+    pvmesh.point_data["rho"] = vis.scalar_fn_to_pv_points(pvmesh, density)
+    pvmesh.point_data["visc"] = vis.scalar_fn_to_pv_points(pvmesh, sympy.log(viscosity))
+    
+
+    with swarm.access():
+        point_cloud.point_data["M"] = material.data.copy()
+
+    pl = pv.Plotter(window_size=(1000, 750))
+
+    # pl.add_points(point_cloud, color="Black",
+    #                   render_points_as_spheres=False,
+    #                   point_size=2.5, opacity=0.75)
+
+    pl.add_mesh(
+                pvmesh,
+                cmap="coolwarm",
+                edge_color="Black",
+                show_edges=True,
+                scalars="visc",
+                use_transparency=False,
+                opacity=0.95,
+               )
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
# Create Stokes object
+
+stokes = uw.systems.Stokes(
+    meshbox, velocityField=v_soln, pressureField=p_soln, solver_name="stokes"
+)
+
+# Set some things
+import sympy
+from sympy import Piecewise
+
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.viscosity = viscosity
+
+stokes.bodyforce = sympy.Matrix([0, -density])
+stokes.saddle_preconditioner = 1.0 / viscosity
+
+# free slip.
+# note with petsc we always need to provide a vector of correct cardinality.
+
+stokes.add_dirichlet_bc((sympy.oo,0.0), "Bottom")
+stokes.add_dirichlet_bc((sympy.oo, 0.0), "Top")
+stokes.add_dirichlet_bc((0.0,sympy.oo), "Left")
+stokes.add_dirichlet_bc((0.0,sympy.oo), "Right")
+
+
+
+
+
+
+
stokes.petsc_options["snes_rtol"] = 1.0e-3
+stokes.petsc_options[
+    "snes_atol"
+] = 1.0e-5  # Not sure why rtol does not do its job when guess is used
+
+# stokes.petsc_options["fieldsplit_velocity_ksp_monitor"] = None
+# stokes.petsc_options["fieldsplit_pressure_ksp_monitor"] = None
+stokes.petsc_options["fieldsplit_velocity_ksp_rtol"] = 1.0e-3
+stokes.petsc_options["fieldsplit_pressure_ksp_rtol"] = 1.0e-2
+
+
+
+
+
+
+
stokes.solve(zero_init_guess=True)
+
+
+
+
+
+
+
# check the solution
+
+if uw.mpi.size == 1 and render:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshbox)
+    pvmesh.point_data["rho"] = vis.scalar_fn_to_pv_points(pvmesh, density)
+    pvmesh.point_data["visc"] = vis.scalar_fn_to_pv_points(pvmesh, sympy.log(viscosity))
+    pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)
+    pvmesh.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh, v_soln.sym.dot(v_soln.sym))
+
+    # point sources at cell centres
+
+    cpoints = np.zeros((meshbox._centroids.shape[0] // 4, 3))
+    cpoints[:, 0] = meshbox._centroids[::4, 0]
+    cpoints[:, 1] = meshbox._centroids[::4, 1]
+    cpoint_cloud = pv.PolyData(cpoints)
+
+    pvstream = pvmesh.streamlines_from_source(
+        cpoint_cloud,
+        vectors="V",
+        integrator_type=45,
+        integration_direction="forward",
+        compute_vorticity=False,
+        max_steps=25,
+        surface_streamlines=True,
+    )
+    
+    spoints = vis.swarm_to_pv_cloud(swarm)
+    spoint_cloud = pv.PolyData(spoints)
+
+    with swarm.access():
+        spoint_cloud.point_data["M"] = material.data[...]
+
+    pl = pv.Plotter(window_size=(1000, 750))
+
+    pl.add_mesh(pvstream, opacity=1)
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Gray",
+        show_edges=True,
+        scalars="rho",
+        opacity=0.5,
+    )
+
+    pl.add_points(
+        spoint_cloud,
+        cmap="gray_r",
+        scalars="M",
+        render_points_as_spheres=True,
+        point_size=5,
+        opacity=0.33,
+    )
+
+    # pl.add_points(pdata)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
def plot_mesh(filename):
+    if uw.mpi.size == 1:
+        
+        import pyvista as pv
+        import underworld3.visualisation as vis
+
+        pvmesh = vis.mesh_to_pv_mesh(meshbox)
+        pvmesh.point_data["rho"] = vis.scalar_fn_to_pv_points(pvmesh, density)
+        pvmesh.point_data["visc"] = vis.scalar_fn_to_pv_points(pvmesh, sympy.log(viscosity))
+        pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)
+        pvmesh.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh, v_soln.sym.dot(v_soln.sym))
+
+        # point sources at cell centres
+
+        cpoints = np.zeros((meshbox._centroids.shape[0], 3))
+        cpoints[:, 0] = meshbox._centroids[:, 0]
+        cpoints[:, 1] = meshbox._centroids[:, 1]
+        cpoint_cloud = pv.PolyData(cpoints)
+
+        pvstream = pvmesh.streamlines_from_source(
+            cpoint_cloud,
+            vectors="V",
+            integrator_type=45,
+            integration_direction="forward",
+            compute_vorticity=False,
+            max_steps=25,
+            surface_streamlines=True,
+        )
+
+        spoints = vis.swarm_to_pv_cloud(swarm)
+        spoint_cloud = pv.PolyData(spoints)
+
+        with swarm.access():
+            spoint_cloud.point_data["M"] = material.data[...]
+
+        pl = pv.Plotter()
+
+        # pl.add_mesh(pvmesh, "Gray",  "wireframe")
+        # pl.add_arrows(arrow_loc, velocity_field, mag=0.2/vmag, opacity=0.5)
+
+        pl.add_mesh(pvstream, opacity=1)
+        pl.add_mesh(
+            pvmesh,
+            cmap="coolwarm",
+            edge_color="Gray",
+            show_edges=True,
+            scalars="visc",
+            opacity=0.5,
+        )
+
+        pl.add_points(
+            spoint_cloud,
+            cmap="gray_r",
+            scalars="M",
+            render_points_as_spheres=True,
+            point_size=5,
+            opacity=0.33,
+        )
+
+        # pl.add_points(pdata)
+
+        pl.remove_scalar_bar("M")
+        pl.remove_scalar_bar("visc")
+
+        pl.screenshot(
+            filename="{}.png".format(filename),
+            window_size=(1250, 1250),
+            return_img=False,
+        )
+
+        # pl.show()
+        pv.close_all()
+
+        return
+
+
+
+
+
+
+
t_step = 0
+
+
+
+
+
+
+
# Update in time
+
+expt_name = "output/blobs"
+
+for step in range(0, 2): # 250
+    stokes.solve(zero_init_guess=False)
+    delta_t = min(10.0, stokes.estimate_dt())
+
+    # update swarm / swarm variables
+
+    if uw.mpi.rank == 0:
+        print("Timestep {}, dt {}".format(t_step, delta_t))
+
+    # advect swarm
+    print("Swarm Advection")
+    swarm.advection(v_soln.fn, delta_t)
+    print("Swarm Advection - done")
+
+    if t_step % 1 == 0:
+        plot_mesh(filename="{}_step_{}".format(expt_name, t_step))
+
+    t_step += 1
+
+
+
+
+
+
+
meshbox.petsc_save_checkpoint(index=step, meshVars=[v_soln], outputPath='./output/')
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Swarm_RT_Cartesian.html b/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Swarm_RT_Cartesian.html new file mode 100644 index 0000000..468e12d --- /dev/null +++ b/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Swarm_RT_Cartesian.html @@ -0,0 +1,990 @@ + + + + + + + + + + + Rayleigh Taylor - swarm materials — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Rayleigh Taylor - swarm materials

+ +
+
+ +
+
+
+ + + + +
+ +
+

Rayleigh Taylor - swarm materials#

+

We introduce the notion of an IndexSwarmVariable which automatically generates masks for a swarm +variable that consists of discrete level values (integers).

+

For a variable \(M\), the mask variables are \(\left\{ M^0, M^1, M^2 \ldots M^{N-1} \right\}\) where \(N\) is the number of indices (e.g. material types) on the variable. This value must be defined in advance.

+

The masks are orthogonal in the sense that \(M^i * M^j = 0\) if \(i \ne j\), and they are complete in the sense that \(\sum_i M^i = 1\) at all points.

+

The masks are implemented as continuous mesh variables (the user can specify the interpolation order) and so they are also differentiable (once).

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+
+import underworld3 as uw
+from underworld3.systems import Stokes
+from underworld3 import function
+
+import numpy as np
+import sympy
+
+render = True
+
+cell_size = uw.options.getReal("mesh_cell_size", default=1.0 / 32)
+particle_fill = uw.options.getInt("particle_fill", default=7)
+viscosity_ratio = uw.options.getReal("rt_viscosity_ratio", default=1.0)
+
+
+
+
+
+
+
lightIndex = 0
+denseIndex = 1
+
+boxLength = 0.9142
+boxHeight = 1.0
+viscosityRatio = viscosity_ratio
+amplitude = 0.02
+offset = 0.2
+model_end_time = 300.0
+
+# material perturbation from van Keken et al. 1997
+wavelength = 2.0 * boxLength
+k = 2.0 * np.pi / wavelength
+
+
+
+
+
+
+
meshbox = uw.meshing.UnstructuredSimplexBox(
+    minCoords=(0.0, 0.0),
+    maxCoords=(boxLength, boxHeight),
+    cellSize=cell_size,
+    regular=False,
+    qdegree=2,
+)
+
+
+
+
+
+
+
import sympy
+
+# Some useful coordinate stuff
+
+x, y = meshbox.CoordinateSystem.X
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable(r"U", meshbox, meshbox.dim, degree=2)
+p_soln = uw.discretisation.MeshVariable(r"P", meshbox, 1, degree=1)
+m_cont = uw.discretisation.MeshVariable(r"M_c", meshbox, 1, degree=1, continuous=True)
+
+
+
+
+
+
+
swarm = uw.swarm.Swarm(mesh=meshbox)
+material = uw.swarm.IndexSwarmVariable(
+    r"M", swarm, indices=2, proxy_degree=1, proxy_continuous=False
+)
+swarm.populate(fill_param=particle_fill)
+
+
+
+
+
+
+
with swarm.access(material):
+    material.data[...] = 0
+
+with swarm.access(material):
+    perturbation = offset + amplitude * np.cos(
+        k * swarm.particle_coordinates.data[:, 0]
+    )
+    material.data[:, 0] = np.where(
+        perturbation > swarm.particle_coordinates.data[:, 1], lightIndex, denseIndex
+    )
+
+material.sym
+
+
+
+
+
+
+
# print(f"Memory usage = {python_process.memory_info().rss//1000000} Mb", flush=True)
+
+
+
+
+
+
+
X = meshbox.CoordinateSystem.X
+
+
+
+
+
+
+
mat_density = np.array([0, 1])  # lightIndex, denseIndex
+density = mat_density[0] * material.sym[0] + mat_density[1] * material.sym[1]
+
+
+
+
+
+
+
mat_viscosity = np.array([viscosityRatio, 1])
+viscosity = mat_viscosity[0] * material.sym[0] + mat_viscosity[1] * material.sym[1]
+
+
+
+
+
+
+
# Create Stokes object
+
+stokes = uw.systems.Stokes(
+    meshbox, velocityField=v_soln, pressureField=p_soln, solver_name="stokes"
+)
+
+# Set some things
+import sympy
+from sympy import Piecewise
+
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.viscosity = viscosity
+
+stokes.bodyforce = sympy.Matrix([0, -density])
+stokes.saddle_preconditioner = 1.0 / viscosity
+
+# free slip.
+# note with petsc we always need to provide a vector of correct cardinality.
+
+stokes.add_dirichlet_bc((sympy.oo,0.0), "Bottom")
+stokes.add_dirichlet_bc((sympy.oo, 0.0), "Top")
+stokes.add_dirichlet_bc((0.0,sympy.oo), "Left")
+stokes.add_dirichlet_bc((0.0,sympy.oo), "Right")
+
+
+
+
+
+
+
stokes.rtol = 1.0e-3  # rough solution is all that's needed
+
+
+
+
+
+
+
print("Stokes setup", flush=True)
+
+
+
+
+
+
+
m_solver = uw.systems.Projection(meshbox, m_cont)
+m_solver.uw_function = material.sym[1]
+m_solver.smoothing = 1.0e-3
+m_solver.solve()
+
+
+
+
+
+
+
print("Solve projection ... done", flush=True)
+
+
+
+
+
+
+
# stokes._setup_terms()
+
+
+
+
+
+
+
print("Stokes terms ... done", flush=True)
+
+
+
+
+
+
+
stokes.solve(zero_init_guess=True)
+
+
+
+
+
+
+
# check the solution
+
+if uw.mpi.size == 1 and render:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshbox)
+    pvmesh.point_data["rho"] = vis.scalar_fn_to_pv_points(pvmesh, density)
+    pvmesh.point_data["visc"] = vis.scalar_fn_to_pv_points(pvmesh, sympy.log(viscosity))
+    pvmesh.point_data["M"] = vis.scalar_fn_to_pv_points(pvmesh, m_cont.sym)
+    pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)
+
+
+    # point sources at cell centres
+    cpoints = np.zeros((meshbox._centroids[::4].shape[0], 3))
+    cpoints[:, 0] = meshbox._centroids[::4, 0]
+    cpoints[:, 1] = meshbox._centroids[::4, 1]
+    cpoint_cloud = pv.PolyData(cpoints)
+
+    pvstream = pvmesh.streamlines_from_source(
+        cpoint_cloud,
+        vectors="V",
+        integrator_type=45,
+        integration_direction="forward",
+        compute_vorticity=False,
+        max_steps=25,
+        surface_streamlines=True,
+    )
+
+    spoints = vis.swarm_to_pv_cloud(swarm)
+    spoint_cloud = pv.PolyData(spoints)
+
+    with swarm.access():
+        spoint_cloud.point_data["M"] = material.data[...]
+
+    
+    pl = pv.Plotter(window_size=(1000, 750))
+
+    pl.add_mesh(pvstream, opacity=1.0)
+    pl.add_mesh(
+        pvmesh,
+        cmap="Blues_r",
+        edge_color="Gray",
+        show_edges=True,
+        scalars="M",
+        opacity=0.75,
+    )
+    pl.add_points(
+        spoint_cloud,
+        cmap="Reds_r",
+        scalars="M",
+        render_points_as_spheres=True,
+        point_size=3,
+        opacity=0.5,
+    )
+
+    # pl.add_points(pdata)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+

+def plot_mesh(filename):
+    if uw.mpi.size == 1:
+        import pyvista as pv
+        import underworld3.visualisation as vis
+    
+        pvmesh = vis.mesh_to_pv_mesh(meshbox)
+        pvmesh.point_data["rho"] = vis.scalar_fn_to_pv_points(pvmesh, density)
+        pvmesh.point_data["visc"] = vis.scalar_fn_to_pv_points(pvmesh, sympy.log(viscosity))
+        pvmesh.point_data["M"] = vis.scalar_fn_to_pv_points(pvmesh, m_cont.sym)
+        pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)
+
+        # point sources at cell centres
+        subsample = 3
+        cpoints = np.zeros((meshbox._centroids[::subsample].shape[0], 3))
+        cpoints[:, 0] = meshbox._centroids[::subsample, 0]
+        cpoints[:, 1] = meshbox._centroids[::subsample, 1]
+        cpoint_cloud = pv.PolyData(cpoints)
+
+        pvstream = pvmesh.streamlines_from_source(
+            cpoint_cloud,
+            vectors="V",
+            integrator_type=45,
+            integration_direction="forward",
+            compute_vorticity=False,
+            max_steps=25,
+            surface_streamlines=True,
+        )
+
+        spoints = vis.swarm_to_pv_cloud(swarm)
+        spoint_cloud = pv.PolyData(spoints)
+
+        with swarm.access():
+            spoint_cloud.point_data["M"] = material.data[...]
+
+        pl.clear()
+
+        # pl.add_mesh(pvmesh, "Gray",  "wireframe")
+        # pl.add_arrows(arrow_loc, velocity_field, mag=0.2/vmag, opacity=0.5)
+
+        pl.add_mesh(pvstream, opacity=1)
+        pl.add_mesh(
+            pvmesh,
+            cmap="Blues_r",
+            edge_color="Gray",
+            show_edges=True,
+            scalars="M",
+            opacity=0.75,
+        )
+
+        pl.add_points(
+            spoint_cloud,
+            cmap="Reds_r",
+            scalars="M",
+            render_points_as_spheres=True,
+            point_size=3,
+            opacity=0.3,
+        )
+
+        pl.remove_scalar_bar("M")
+        pl.remove_scalar_bar("V")
+        # pl.remove_scalar_bar("rho")
+
+        pl.screenshot(
+            filename="{}.png".format(filename),
+            window_size=(1250, 1250),
+            return_img=False,
+        )
+
+        return
+
+
+
+
+
+
+
t_step = 0
+
+
+
+
+
+
+
!mkdir output
+
+
+
+
+
+
+
# Update in time
+
+expt_name = "swarm_rt"
+
+for step in range(0, 2): #250
+    stokes.solve(zero_init_guess=False)
+    m_solver.solve(zero_init_guess=False)
+    delta_t = min(10.0, stokes.estimate_dt())
+
+    # update swarm / swarm variables
+
+    if uw.mpi.rank == 0:
+        print("Timestep {}, dt {}".format(t_step, delta_t))
+
+    # advect swarm
+    swarm.advection(v_soln.sym, delta_t)
+
+    if t_step % 5 == 0:
+        plot_mesh(filename="{}_step_{}".format(expt_name, t_step))
+
+        # "Checkpoints"
+        savefile = f"swarm_rt_xy"
+
+        meshbox.write_timestep(
+            expt_name,
+            meshUpdates=True,
+            meshVars=[p_soln, v_soln, m_cont],
+            outputPath="output",
+            index=t_step,
+        )
+
+    t_step += 1
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Swarm_RT_Spherical.html b/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Swarm_RT_Spherical.html new file mode 100644 index 0000000..88bfa9c --- /dev/null +++ b/main/Notebooks/Examples-StokesFlow/Ex_Stokes_Swarm_RT_Spherical.html @@ -0,0 +1,990 @@ + + + + + + + + + + + Rayleigh-Taylor (Level-set based) in the sphere — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Rayleigh-Taylor (Level-set based) in the sphere

+ +
+
+ +
+
+
+ + + + +
+ +
+

Rayleigh-Taylor (Level-set based) in the sphere#

+

If there are just two materials, then an efficient way to manage the interface tracking is through a “level-set” which tracks not just the material type, but the distance to the interface. The distance is a continuous quantity that is not degraded quickly by classical advection schemes. A particle-based level set also has advantages because the smooth signed-distance quantity can be projected to the mesh more accurately than a sharp condition function.

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import os
+
+os.environ["UW_TIMING_ENABLE"] = "1"
+
+import petsc4py
+from petsc4py import PETSc
+
+import underworld3 as uw
+from underworld3.systems import Stokes
+from underworld3 import function
+from underworld3 import timing
+
+import numpy as np
+import sympy
+
+render = True
+
+
+
+
+
+
+
lightIndex = 0
+denseIndex = 1
+
+viscosityRatio = 1.0
+
+r_layer = 0.7
+r_o = 1.0
+r_i = 0.54
+
+res = 0.25
+
+Rayleigh = 1.0e6 / (r_o - r_i) ** 3
+
+offset = 0.5 * res
+
+
+
+
+
+
+
cell_size = uw.options.getReal("mesh_cell_size", default=0.1)
+particle_fill = uw.options.getInt("particle_fill", default=5)
+viscosity_ratio = uw.options.getReal("rt_viscosity_ratio", default=1.0)
+
+
+mesh = uw.meshing.SphericalShell(
+    radiusInner=r_i, radiusOuter=r_o, cellSize=res, qdegree=2
+)
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable(r"U", mesh, mesh.dim, degree=2)
+p_soln = uw.discretisation.MeshVariable(r"P", mesh, 1, degree=1)
+meshr = uw.discretisation.MeshVariable(r"r", mesh, 1, degree=1)
+
+
+
+
+
+
+
swarm = uw.swarm.Swarm(mesh=mesh)
+material = uw.swarm.SwarmVariable(r"\cal{L}", swarm, proxy_degree=1, size=1)
+swarm.populate(fill_param=2)
+
+
+
+
+
+
+
with swarm.access(material):
+    r = np.sqrt(
+        swarm.particle_coordinates.data[:, 0] ** 2
+        + swarm.particle_coordinates.data[:, 1] ** 2
+        + (swarm.particle_coordinates.data[:, 2] - offset) ** 2
+    )
+
+    material.data[:, 0] = r - r_layer
+
+
+
+
+
+
+

+# Some useful coordinate stuff
+
+x, y, z = mesh.CoordinateSystem.X
+ra, l1, l2 = mesh.CoordinateSystem.xR
+
+
+
+
+
+
+

+density = sympy.Piecewise((0.0, material.sym[0] < 0.0), (1.0, True))
+display(density)
+
+viscosity = sympy.Piecewise((1.0, material.sym[0] < 0.0), (1.0, True))
+display(viscosity)
+
+
+
+
+
+
+
with swarm.access():
+    print(material.data.max(), material.data.min())
+
+
+
+
+
+
+
if False:
+    import numpy as np
+    import pyvista as pv
+    import vtk
+
+    pv.global_theme.background = "white"
+    pv.global_theme.window_size = [750, 750]
+    pv.global_theme.antialiasing = True
+    pv.global_theme.jupyter_backend = "panel"
+    pv.global_theme.smooth_shading = True
+
+    mesh.vtk("tmp_mesh.vtk")
+    pvmesh = pv.read("tmp_mesh.vtk")
+
+    with swarm.access():
+        points = np.zeros((swarm.data.shape[0], 3))
+        points[:, 0] = swarm.data[:, 0]
+        points[:, 1] = swarm.data[:, 1]
+        points[:, 2] = swarm.data[:, 2]
+
+    point_cloud = pv.PolyData(points)
+
+    with mesh.access():
+        pvmesh.point_data["M"] = uw.function.evaluate(material.sym[0], mesh.data)
+        pvmesh.point_data["rho"] = uw.function.evaluate(density, mesh.data)
+        pvmesh.point_data["visc"] = uw.function.evaluate(
+            sympy.log(viscosity), mesh.data
+        )
+
+    with swarm.access():
+        point_cloud.point_data["M"] = material.data.copy()
+
+    pl = pv.Plotter()
+
+    pl.add_mesh(pvmesh, "Black", "wireframe")
+
+    pl.add_points(
+        point_cloud,
+        cmap="coolwarm",
+        scalars="M",
+        render_points_as_spheres=True,
+        point_size=2,
+        opacity=0.5,
+    )
+
+    # pl.add_mesh(
+    #     pvmesh,
+    #     cmap="coolwarm",
+    #     edge_color="Black",
+    #     show_edges=True,
+    #     scalars="M1",
+    #     use_transparency=False,
+    #     opacity=0.25,
+    # )
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
stokes = uw.systems.Stokes(
+    mesh,
+    velocityField=v_soln,
+    pressureField=p_soln,
+    verbose=False,
+    solver_name="stokes",
+)
+
+# stokes.petsc_options.delValue("ksp_monitor") # We can flip the default behaviour at some point
+stokes.petsc_options["snes_rtol"] = 1.0e-4
+stokes.petsc_options["snes_rtol"] = 1.0e-3
+stokes.petsc_options["ksp_monitor"] = None
+
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.viscosity = viscosity
+
+Gamma = mesh.Gamma
+stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) *  Gamma, "Upper")
+stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) *  Gamma, "Lower")
+
+# buoyancy (magnitude)
+buoyancy = Rayleigh * density  # * (1 - surface_fn) * (1 - base_fn)
+
+unit_vec_r = mesh.CoordinateSystem.X / mesh.CoordinateSystem.xR[0]
+
+# Free slip condition by penalizing radial velocity at the surface (non-linear term)
+
+stokes.bodyforce = -unit_vec_r * buoyancy
+
+stokes.saddle_preconditioner = 1 / viscosity
+
+
+
+
+
+
+
mesh.CoordinateSystem.unit_e_0.shape
+(mesh.CoordinateSystem.X / mesh.CoordinateSystem.xR[0]).shape
+
+
+
+
+
+
+
with mesh.access(meshr):
+    meshr.data[:, 0] = uw.function.evaluate(
+        sympy.sqrt(x**2 + y**2 + z**2), mesh.data, mesh.N
+    )  # cf radius_fn which is 0->1
+
+
+
+
+
+
+
timing.reset()
+timing.start()
+
+stokes.solve(zero_init_guess=True)
+
+timing.print_table()
+
+
+
+
+
+
+
# check the solution
+
+if uw.mpi.size == 1 and render:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh)
+    pvmesh.point_data["rho"] = vis.scalar_fn_to_pv_points(pvmesh, density)
+    pvmesh.point_data["M"] = vis.scalar_fn_to_pv_points(pvmesh, material.sym)
+    pvmesh.point_data["V"] = 10.0 * vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)/vis.vector_fn_to_pv_points(pvmesh, v_soln.sym).max()
+
+    # point sources at cell centres
+
+    subsample = 2
+
+    cpoints = np.zeros((mesh._centroids[::subsample, 0].shape[0], 3))
+    cpoints[:, 0] = mesh._centroids[::subsample, 0]
+    cpoints[:, 1] = mesh._centroids[::subsample, 1]
+    cpoints[:, 2] = mesh._centroids[::subsample, 2]
+
+    cpoint_cloud = pv.PolyData(cpoints)
+
+    pvstream = pvmesh.streamlines_from_source(
+        cpoint_cloud,
+        vectors="V",
+        integrator_type=45,
+        integration_direction="both",
+        compute_vorticity=False,
+        surface_streamlines=False,
+    )
+
+    spoints = vis.swarm_to_pv_cloud(swarm)
+    spoint_cloud = pv.PolyData(spoints)
+
+    with swarm.access():
+        spoint_cloud.point_data["M"] = material.data[...]
+
+    contours = pvmesh.contour(isosurfaces=[0.0], scalars="M")
+
+    pl = pv.Plotter(window_size=(1000, 1000))
+
+    pl.add_mesh(pvmesh, "Black", "wireframe", opacity=0.5)
+    # pl.add_arrows(arrow_loc, velocity_field, mag=0.2/vmag, opacity=0.5)
+
+    pl.add_mesh(pvstream, opacity=1.0, cmap="RdGy_r",)
+    # pl.add_mesh(pvmesh, cmap="Blues_r", edge_color="Gray", show_edges=True, scalars="rho", opacity=0.25)
+
+    # pl.add_mesh(contours, opacity=1, color="Blue")
+
+    pl.add_points(spoint_cloud, cmap="Reds_r", scalars="M", render_points_as_spheres=True, point_size=10, opacity=0.3)
+    # pl.add_points(pdata)
+
+    pl.show(cpos="xz")
+
+
+
+
+
+
+

+def plot_mesh(filename):
+    if uw.mpi.size != 1:
+        return
+
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh)
+    pvmesh.point_data["rho"] = vis.scalar_fn_to_pv_points(pvmesh, density)
+    pvmesh.point_data["M"] = vis.scalar_fn_to_pv_points(pvmesh, material.sym)
+    pvmesh.point_data["V"] = 10.0 * vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)/vis.vector_fn_to_pv_points(pvmesh, v_soln.sym).max()
+    print(f"Vscale {vis.vector_fn_to_pv_points(pvmesh, v_soln.sym).max()}")
+
+    # point sources at cell centres
+
+    cpoints = np.zeros((mesh._centroids[::2].shape[0], 3))
+    cpoints[:, 0] = mesh._centroids[::2, 0]
+    cpoints[:, 1] = mesh._centroids[::2, 1]
+    cpoint_cloud = pv.PolyData(cpoints)
+
+    pvstream = pvmesh.streamlines_from_source(
+        cpoint_cloud,
+        vectors="V",
+        integrator_type=45,
+        integration_direction="both",
+        compute_vorticity=False,
+        surface_streamlines=False,
+    )
+
+    spoints = vis.swarm_to_pv_cloud(swarm)
+    spoint_cloud = pv.PolyData(spoints)
+
+    with swarm.access():
+        spoint_cloud.point_data["M"] = material.data[...]
+
+    contours = pvmesh.contour(isosurfaces=[0.0], scalars="M")
+
+    ## Plotting into existing pl (memory leak in pyvista)
+    pl.clear()
+
+    pl.add_mesh(pvmesh, "Gray", "wireframe")
+    # pl.add_arrows(arrow_loc, velocity_field, mag=0.2/vmag, opacity=0.5)
+
+    pl.add_mesh(pvstream, opacity=0.33)
+    # pl.add_mesh(pvmesh, cmap="Blues_r", edge_color="Gray", show_edges=True, scalars="rho", opacity=0.25)
+
+    # pl.add_points(
+    #     spoint_cloud, cmap="Reds_r", scalars="M", render_points_as_spheres=True, point_size=2, opacity=0.3
+    # )
+
+    pl.add_mesh(contours, opacity=0.75, color="Yellow")
+
+    # pl.remove_scalar_bar("Mat")
+    pl.remove_scalar_bar("V")
+    # pl.remove_scalar_bar("rho")
+
+    pl.camera_position = "xz"
+    pl.screenshot(
+        filename="{}.png".format(filename),
+        window_size=(1000, 1000),
+        return_img=False,
+    )
+
+    return
+
+
+
+
+
+
+
t_step = 0
+
+
+
+
+
+
+
# Update in time
+
+expt_name = "output/swarm_rt_sph"
+
+for step in range(0, 10):
+    stokes.solve(zero_init_guess=False)
+    delta_t = 2.0 * stokes.estimate_dt()
+
+    # update swarm / swarm variables
+
+    if uw.mpi.rank == 0:
+        print("Timestep {}, dt {}".format(t_step, delta_t))
+
+    # advect swarm
+    swarm.advection(v_soln.sym, delta_t)
+
+    if t_step < 10 or t_step % 5 == 0:
+        # plot_mesh(filename="{}_step_{}".format(expt_name, t_step))
+
+        mesh.petsc_save_checkpoint(index=t_step, meshVars=[v_soln], outputPath='./output/')
+
+    t_step += 1
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-StokesFlow/Ex_WIP_Stokes_Periodic.html b/main/Notebooks/Examples-StokesFlow/Ex_WIP_Stokes_Periodic.html new file mode 100644 index 0000000..95b586a --- /dev/null +++ b/main/Notebooks/Examples-StokesFlow/Ex_WIP_Stokes_Periodic.html @@ -0,0 +1,973 @@ + + + + + + + + + + + Periodic Mesh Example (WIP) — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Periodic Mesh Example (WIP)

+ +
+ +
+
+ + + + +
+ +
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+

Periodic Mesh Example (WIP)#

+

This is a periodic, Cartesian mesh with the periodic bcs specified using gmsh itself. +Compare this to the Cylindrical Stokes example that has periodic coordinates in a mesh +that is continuously connected.

+
+

Generate Periodic mesh using GMSH#

+
+
+
# %%
+import gmsh
+
+
+
+
+
+
+
gmsh.initialize()
+
+
+
+
+
+
+
# %%
+gmsh.model.add("Periodic x")
+
+
+
+
+
+
+
# %%
+minCoords = (0.0, 0.0)
+maxCoords = (1.0, 1.0)
+cellSize = 0.1
+
+
+
+
+
+
+
# %%
+boundaries = {
+    "Bottom": 1,
+    "Top": 2,
+    "Right": 3,
+    "Left": 4,
+}
+
+
+
+
+
+
+
# %%
+xmin, ymin = minCoords
+xmax, ymax = maxCoords
+
+
+
+
+
+
+
# %%
+p1 = gmsh.model.geo.add_point(xmin, ymin, 0.0, meshSize=cellSize)
+p2 = gmsh.model.geo.add_point(xmax, ymin, 0.0, meshSize=cellSize)
+p3 = gmsh.model.geo.add_point(xmin, ymax, 0.0, meshSize=cellSize)
+p4 = gmsh.model.geo.add_point(xmax, ymax, 0.0, meshSize=cellSize)
+
+
+
+
+
+
+
l1 = gmsh.model.geo.add_line(p1, p2, tag=boundaries["Bottom"])
+l2 = gmsh.model.geo.add_line(p2, p4, tag=boundaries["Right"])
+l3 = gmsh.model.geo.add_line(p4, p3, tag=boundaries["Top"])
+l4 = gmsh.model.geo.add_line(p3, p1, tag=boundaries["Left"])
+
+
+
+
+
+
+
cl = gmsh.model.geo.add_curve_loop((l1, l2, l3, l4))
+surface = gmsh.model.geo.add_plane_surface([cl])
+
+
+
+
+
+
+
# %%
+gmsh.model.geo.synchronize()
+
+
+
+
+
+
+
# %%
+translation = [1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
+
+
+
+
+
+
+
# %%
+gmsh.model.mesh.setPeriodic(1, [boundaries["Right"]], [boundaries["Left"]], translation)
+
+
+
+
+
+
+
# %%
+# Add Physical groups
+for name, tag in boundaries.items():
+    gmsh.model.add_physical_group(1, [tag], tag)
+    gmsh.model.set_physical_name(1, tag, name)
+
+
+
+
+
+
+
gmsh.model.addPhysicalGroup(2, [surface], surface)
+gmsh.model.setPhysicalName(2, surface, "Elements")
+
+
+
+
+
+
+
# %%
+gmsh.model.mesh.generate(2)
+gmsh.write("tmp_periodicx.msh")
+gmsh.finalize()
+
+
+
+
+
+
+

Import Mesh into PETSc#

+
+
+
# %%
+from petsc4py import PETSc
+import underworld3 as uw
+from underworld3.systems import Stokes
+import numpy as np
+
+
+
+
+
+
+
options = PETSc.Options()
+
+
+
+
+
+
+
# %%
+plex = PETSc.DMPlex().createFromFile("tmp_periodicx.msh")
+
+
+
+
+
+
+
# %%
+for name, tag in boundaries.items():
+    plex.createLabel(name)
+    label = plex.getLabel(name)
+    indexSet = plex.getStratumIS("Face Sets", tag)
+    if indexSet:
+        label.insertIS(indexSet, 1)
+    else:
+        plex.removeLabel(name)
+
+
+
+
+
+
+
plex.removeLabel("Face Sets")
+
+
+
+
+
+
+
# %%
+plex.view()
+
+
+
+
+
+
+
# %%
+from underworld3.discretisation import Mesh
+
+
+
+
+
+
+
# %%
+mesh = Mesh(plex, degree=1)
+
+
+
+
+
+
+
# %%
+mesh.dm.view()
+
+
+
+
+
+
+
# %%
+swarm = uw.swarm.Swarm(mesh=mesh)
+material = uw.swarm.IndexSwarmVariable("M", swarm, indices=2, proxy_degree=1)
+swarm.populate(fill_param=3)
+
+
+
+
+

Create Stokes object

+
+
+
v = uw.discretisation.MeshVariable("U", mesh, mesh.dim, degree=2)
+p = uw.discretisation.MeshVariable("P", mesh, 1, degree=1)
+
+
+
+
+
+
+
stokes = uw.systems.Stokes(mesh, velocityField=v, pressureField=p)
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.viscosity = 1
+
+
+
+
+
+
+
# No slip boundary conditions
+stokes.add_dirichlet_bc((0.5, 0.0), "Top", (0, 1))
+stokes.add_dirichlet_bc((-0.5, 0.0), "Bottom", (0, 1))
+
+
+
+
+
+
+
# %%
+# Write density into a variable for saving
+densvar = uw.discretisation.MeshVariable("density", mesh, 1)
+with mesh.access(densvar):
+    densvar.data[:, 0] = 1.0
+
+
+
+
+
+
+
# %%
+swarm.dm.getCoordinates().array
+
+
+
+
+
+
+
# %%
+# body force
+import sympy
+
+
+
+
+
+
+
x, y = mesh.X
+
+
+
+
+
+
+
unit_rvec = mesh.rvec / sympy.sqrt(mesh.rvec.dot(mesh.rvec))
+stokes.bodyforce = 0 * mesh.X  # -mesh.X / sympy.sqrt(x**2 + y**2)
+
+
+
+
+
+
+
# %%
+# Solve time
+stokes.solve()
+
+
+
+
+
+
+
mesh.petsc_save_checkpoint(index=0, meshVars=[stokes.u, stokes.p, densvar], outputPath='./output/')
+swarm.petsc_save_checkpoint(swarmName='swarm', index=0, outputPath='./output/')
+
+
+
+
+

check if that works

+
+
+
if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh("tmp_periodicx.msh")
+    # pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v.sym)
+
+    velocity_points = vis.meshVariable_to_pv_cloud(v)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v.sym)
+    
+    pl = pv.Plotter(window_size=(1000, 750))
+
+    pl.add_mesh(pvmesh, "Black", "wireframe")
+    # pl.add_arrows(pvmesh.points, pvmesh.point_data["V"], mag=5.0e-1, opacity=0.5)
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=5.0e-1, opacity=0.5)
+    
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
if uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh("tmp_periodicx.msh")
+    pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v.sym)
+
+    # velocity_points = vis.meshVariable_to_pv_cloud(v)
+    # velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v.sym)
+    
+    pl = pv.Plotter(window_size=(1000, 750))
+
+    pl.add_mesh(pvmesh, "Black", "wireframe")
+    pl.add_arrows(pvmesh.points, pvmesh.point_data["V"], mag=5.0e-1, opacity=0.5)
+    # pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=5.0e-1, opacity=0.5)
+    
+    pl.show(cpos="xy")
+
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-StokesFlow/Ex_stokes_sinkingBlock_benchmark.html b/main/Notebooks/Examples-StokesFlow/Ex_stokes_sinkingBlock_benchmark.html new file mode 100644 index 0000000..c07680e --- /dev/null +++ b/main/Notebooks/Examples-StokesFlow/Ex_stokes_sinkingBlock_benchmark.html @@ -0,0 +1,1094 @@ + + + + + + + + + + + Stokes sinker - sinking block — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Stokes sinker - sinking block

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

Stokes sinker - sinking block#

+

Sinking block benchmark as outlined in:

+ +
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
from petsc4py import PETSc
+import underworld3 as uw
+import numpy as np
+import sympy
+
+options = PETSc.Options()
+
+
+
+
+
+
+
sys = PETSc.Sys()
+sys.pushErrorHandler("traceback")
+
+options["snes_converged_reason"] = None
+options["snes_monitor_short"] = None
+
+
+
+
+
+
+
# import unit registry to make it easy to convert between units
+u = uw.scaling.units
+
+### make scaling easier
+ndim = uw.scaling.non_dimensionalise
+dim = uw.scaling.dimensionalise
+
+
+
+
+
+
+
# Set the resolution, a structured quad box of 51x51 is used in the paper
+# res = 51
+res = 21  # use lower res for testing
+
+nsteps = 1  # number of time steps
+swarmGPC = 2  # swarm fill parameter
+render = True  # plot images
+
+
+
+
+
+
+
refLength = 500e3
+refDensity = 3.3e3
+refGravity = 9.81
+refVelocity = (1 * u.centimeter / u.year).to(u.meter / u.second).m  ### 1 cm/yr in m/s
+refViscosity = 1e21
+refPressure = refDensity * refGravity * refLength
+refTime = refViscosity / refPressure
+
+bodyforce = (
+    refDensity * u.kilogram / u.metre**3 * refGravity * u.meter / u.second**2
+)
+
+
+
+
+
+
+
KL = refLength * u.meter
+Kt = refTime * u.second
+KM = bodyforce * KL**2 * Kt**2
+
+scaling_coefficients = uw.scaling.get_coefficients()
+scaling_coefficients["[length]"] = KL
+scaling_coefficients["[time]"] = Kt
+scaling_coefficients["[mass]"] = KM
+scaling_coefficients
+
+
+
+
+
+
+
### fundamental values
+ref_length = uw.scaling.dimensionalise(1.0, u.meter).magnitude
+ref_length_km = uw.scaling.dimensionalise(1.0, u.kilometer).magnitude
+ref_density = uw.scaling.dimensionalise(1.0, u.kilogram / u.meter**3).magnitude
+ref_gravity = uw.scaling.dimensionalise(1.0, u.meter / u.second**2).magnitude
+ref_temp = uw.scaling.dimensionalise(1.0, u.kelvin).magnitude
+ref_velocity = uw.scaling.dimensionalise(1.0, u.meter / u.second).magnitude
+
+### derived values
+ref_time = ref_length / ref_velocity
+ref_time_Myr = dim(1, u.megayear).m
+ref_pressure = ref_density * ref_gravity * ref_length
+ref_stress = ref_pressure
+ref_viscosity = ref_pressure * ref_time
+
+### Key ND values
+ND_gravity = 9.81 / ref_gravity
+
+
+
+
+
+
+
# define some names for our index
+materialLightIndex = 0
+materialHeavyIndex = 1
+
+## Set constants for the viscosity and density of the sinker.
+viscBG = 1e21 / ref_viscosity
+viscBlock = 1e21 / ref_viscosity
+
+## set density of blocks
+densityBG = 3.2e3 / ref_density
+densityBlock = 3.3e3 / ref_density
+
+
+
+
+
+
+
xmin, xmax = 0.0, ndim(500 * u.kilometer)
+ymin, ymax = 0.0, ndim(500 * u.kilometer)
+
+
+
+
+
+
+
xmin, xmax
+
+
+
+
+
+
+
# Set the box min and max coords
+boxCentre_x, boxCentre_y = ndim(250.0 * u.kilometer), ndim(375.0 * u.kilometer)
+
+box_xmin, box_xmax = boxCentre_x - ndim(50 * u.kilometer), boxCentre_x + ndim(
+    50 * u.kilometer
+)
+box_ymin, box_ymax = boxCentre_y - ndim(50 * u.kilometer), boxCentre_y + ndim(
+    50 * u.kilometer
+)
+
+# location of tracer at bottom of sinker
+x_pos = box_xmax - ((box_xmax - box_xmin) / 2.0)
+y_pos = box_ymin
+
+
+
+
+
+
+
# mesh = uw.meshing.UnstructuredSimplexBox(minCoords=(0.0,0.0),
+#                                               maxCoords=(1.0,1.0),
+#                                               cellSize=1.0/res,
+#                                               regular=True)
+
+# mesh = uw.meshing.UnstructuredSimplexBox(minCoords=(xmin, ymin), maxCoords=(xmax, ymax), cellSize=1.0 / res, regular=False)
+
+mesh = uw.meshing.StructuredQuadBox(
+    elementRes=(int(res), int(res)), minCoords=(xmin, ymin), maxCoords=(xmax, ymax)
+)
+
+
+
+
+
+
+
### Create Stokes object
+
+v = uw.discretisation.MeshVariable("U", mesh, mesh.dim, degree=2)
+p = uw.discretisation.MeshVariable("P", mesh, 1, degree=1)
+
+stokes = uw.systems.Stokes(mesh, velocityField=v, pressureField=p)
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+
+
+
+
+
+
+
#### No slip
+sol_vel = sympy.Matrix([0.0, 0.0])
+
+### free slip
+stokes.add_dirichlet_bc(sol_vel, "Left", 0)  # left/right: components, function, markers
+stokes.add_dirichlet_bc(sol_vel, "Right", 0)  # left/right: components, function, markers
+stokes.add_dirichlet_bc(sol_vel, "Top", 1)  # left/right: components, function, markers
+stokes.add_dirichlet_bc(sol_vel, "Bottom", 1)  # left/right: components, function, markers
+
+
+
+
+
+
+
## Solver
+
+stokes.petsc_options["snes_type"] = "newtonls"
+stokes.petsc_options["ksp_type"] = "fgmres"
+
+stokes.petsc_options["snes_monitor"] = None
+stokes.petsc_options["ksp_monitor"] = None
+
+# stokes.petsc_options.setValue("fieldsplit_velocity_pc_type", "mg")
+stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_type", "kaskade")
+stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_cycle_type", "w")
+
+stokes.petsc_options["fieldsplit_velocity_mg_coarse_pc_type"] = "svd"
+stokes.petsc_options[f"fieldsplit_velocity_ksp_type"] = "fcg"
+stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_type"] = "chebyshev"
+stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_max_it"] = 7
+stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_converged_maxits"] = None
+
+# gasm is super-fast ... but mg seems to be bulletproof
+# gamg is toughest wrt viscosity
+
+stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "gamg")
+stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "additive")
+stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v")
+
+# # # mg, multiplicative - very robust ... similar to gamg, additive
+
+stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "mg")
+stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "multiplicative")
+stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v")
+
+
+
+
+
+
+
swarm = uw.swarm.Swarm(mesh=mesh)
+material = uw.swarm.IndexSwarmVariable("M", swarm, indices=2)
+swarm.populate(fill_param=swarmGPC)
+
+
+
+
+
+
+
with swarm.access(material):
+    material.data[...] = materialLightIndex
+    material.data[
+        (swarm.data[:, 0] >= box_xmin)
+        & (swarm.data[:, 0] <= box_xmax)
+        & (swarm.data[:, 1] >= box_ymin)
+        & (swarm.data[:, 1] <= box_ymax)
+    ] = materialHeavyIndex
+
+
+
+
+
+
+
### add tracer for sinker velocity
+tracer = np.zeros(shape=(1, 2))
+tracer[:, 0], tracer[:, 1] = x_pos, y_pos
+
+
+
+
+
+
+
passiveSwarm = uw.swarm.Swarm(mesh)
+passiveSwarm.dm.finalizeFieldRegister()
+passiveSwarm.dm.addNPoints(npoints=len(tracer))
+passiveSwarm.dm.setPointCoordinates(tracer)
+
+
+
+
+
+
+
mat_density = np.array([densityBG, densityBlock])
+
+density = mat_density[0] * material.sym[0] + mat_density[1] * material.sym[1]
+
+
+
+
+
+
+
mat_viscosity = np.array([viscBG, viscBlock])
+
+viscosityMat = mat_viscosity[0] * material.sym[0] + mat_viscosity[1] * material.sym[1]
+
+
+
+
+
+
+
def plot_mat():
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+    
+    pvmesh = vis.mesh_to_pv_mesh(mesh)
+    points = vis.swarm_to_pv_cloud(swarm)
+
+    point_cloud = pv.PolyData(points)
+    with swarm.access():
+        point_cloud.point_data["M"] = material.data.copy()
+
+    pl = pv.Plotter(notebook=True)
+
+    pl.add_mesh(pvmesh, "Black", "wireframe")
+
+    pl.add_mesh(
+        point_cloud,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=False,
+        scalars="M",
+        use_transparency=False,
+        opacity=0.95,
+    )
+
+    pl.show(cpos="xy")
+
+
+if render and uw.mpi.size == 1:
+    plot_mat()
+
+
+
+
+
+
+
### linear solve
+stokes.constitutive_model.Parameters.shear_viscosity_0 = ndim(
+    ref_viscosity * u.pascal * u.second
+)
+stokes.bodyforce = sympy.Matrix([0, -1 * ND_gravity * density])
+
+
+
+
+
+
+
stokes.solve()
+
+
+
+
+
+
+
### add in material-based viscosity
+stokes.constitutive_model.Parameters.viscosity = viscosityMat
+
+
+
+
+
+
+
# stokes.petsc_options.view()
+options["snes_converged_reason"] = None
+options["snes_monitor_short"] = None
+options["snes_test_jacobian"] = None
+options["snes_test_jacobian_view"] = None
+# stokes.petsc_options['snes_test_jacobian'] = None
+# stokes.petsc_options['snes_test_jacobian_view'] = None
+
+
+
+
+
+
+
stokes.bodyforce
+
+
+
+
+
+
+
stokes.constitutive_model.Parameters.viscosity
+
+
+
+
+
+
+
step = 0
+time = 0.0
+tSinker = np.zeros(nsteps + 1) * np.nan
+ySinker = np.zeros(nsteps + 1) * np.nan
+
+
+
+
+
+
+
def record_tracer(step, time):
+    ### Get the position of the sinking ball
+    with passiveSwarm.access(passiveSwarm):
+        if passiveSwarm.dm.getLocalSize() > 0:
+            ymin = passiveSwarm.data[:, 1].min()
+    ySinker[step] = ymin
+    tSinker[step] = time
+
+    ### print some stuff
+    if passiveSwarm.dm.getLocalSize() > 0:
+        print(
+            f"Step: {str(step).rjust(3)}, time: {dim(time, u.megayear).m:6.2f} [Myr], tracer:  {dim(ymin, u.kilometer):6.2f} [km]"
+        )
+
+
+record_tracer(step, time)
+
+while step < nsteps:
+    ### solve stokes
+    stokes.solve()
+    ### estimate dt
+    dt = 0.5 * stokes.estimate_dt()
+
+    ### advect the swarm
+    swarm.advection(stokes.u.sym, dt, corrector=False)
+    passiveSwarm.advection(stokes.u.sym, dt, corrector=False)
+
+    ### advect tracer
+    # vel_on_tracer = uw.function.evaluate(stokes.u.fn,tracer)
+    # tracer += dt*vel_on_tracer
+    step += 1
+    time += dt
+
+    record_tracer(step, time)
+
+
+
+
+
+
+
if passiveSwarm.dm.getLocalSize() > 0:
+    import matplotlib.pyplot as plt
+
+    ### remove nan values, if any. Convert to km and Myr
+    ySinker = dim(ySinker[~np.isnan(ySinker)], u.kilometer)
+    tSinker = dim(tSinker[~np.isnan(tSinker)], u.megayear)
+
+    print("Initial position: t = {0:.3f}, y = {1:.3f}".format(tSinker[0], ySinker[0]))
+    print("Final position:   t = {0:.3f}, y = {1:.3f}".format(tSinker[-1], ySinker[-1]))
+
+    UWvelocity = (
+        ((ySinker[0] - ySinker[-1]) / (tSinker[-1] - tSinker[0]))
+        .to(u.meter / u.second)
+        .m
+    )
+    print(f"Velocity:         v = {UWvelocity} m/s")
+
+    if uw.mpi.size == 0:
+        fig = plt.figure()
+        fig.set_size_inches(12, 6)
+        ax = fig.add_subplot(1, 1, 1)
+        ax.plot(tSinker.m, ySinker.m)
+        ax.set_xlabel("Time [Myr]")
+        ax.set_ylabel("Sinker position [km]")
+
+
+
+
+
+

compare values against published results#

+
    +
  • The marker, representing the velocity calculated from the UW model, should fit along the curved line.

  • +
  • These velocities are taken from Gerya (2010), Introduction to numerical modelling (2nd Ed), page 345, but show the same model referenced in the paper above

  • +
+
+
+
from scipy.interpolate import interp1d
+
+
+#### col 0 is log10( visc_block / visc_BG ), col 1 is block velocity, m/s
+
+paperData = np.array(
+    [
+        (-6.01810758939326, 1.3776991077026654e-9),
+        (-5.014458950015076, 1.3792676876049961e-9),
+        (-4.018123543216514, 1.3794412652019993e-9),
+        (-3.021737084183539, 1.3740399388011341e-9),
+        (-2.0104944249364634, 1.346341549020515e-9),
+        (-1.0053652707603105, 1.1862379129846573e-9),
+        (-0.005609364256097038, 8.128929227244664e-10),
+        (0.993865754958847, 4.702099044525527e-10),
+        (2.005950776073732, 3.505255987071023e-10),
+        (3.0024521026341358, 3.3258073831103253e-10),
+        (4.006139031188129, 3.2996814021496194e-10),
+        (5.00247443798669, 3.301417178119651e-10),
+        (6.013474599120308, 3.289241220212219e-10),
+    ]
+)
+
+### some errors from sampling, so rounding are used
+visc_ratio = np.round(paperData[:, 0])
+paperVelocity = np.round(paperData[:, 1], 11)
+
+f = interp1d(visc_ratio, paperVelocity, kind="cubic")
+x = np.arange(-6, 6, 0.01)
+
+if uw.mpi.size == 1 and render:
+    import matplotlib.pyplot as plt
+
+    plt.title("check benchmark")
+    plt.plot(x, f(x), label="benchmark velocity curve", c="k")
+    plt.scatter(
+        np.log10(viscBlock / viscBG),
+        UWvelocity,
+        label="model velocity",
+        c="red",
+        marker="x",
+    )
+    plt.legend()
+
+
+
+
+
+
+
### for uw testing system
+def test_sinkBlock():
+    if uw.mpi.size == 1:
+        assert np.isclose(f(0.0), UWvelocity)
+
+
+
+
+
+
+
if render and uw.mpi.size == 1:
+    plot_mat()
+
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-StokesFlow/Readme.html b/main/Notebooks/Examples-StokesFlow/Readme.html new file mode 100644 index 0000000..2245624 --- /dev/null +++ b/main/Notebooks/Examples-StokesFlow/Readme.html @@ -0,0 +1,593 @@ + + + + + + + + + + + Stokes Flow — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Stokes Flow

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

Stokes Flow#

+

+
+
+
+

Recent solver and visualization updates#

+
    +
  • [ ] Ex_Stokes_Cartesian_SolNL.py

    +
      +
    • [ ] Some issue with clang compiler. Compiler not able find the analytical solution file.

    • +
    +
  • +
  • [x] Ex_Stokes_Disk_Cartesian-FixedStars.py

  • +
  • [x] Ex_Stokes_Disk_Cartesian.py

  • +
  • [x] Ex_Stokes_Disk_CylCoords.py

  • +
  • [x] Ex_Stokes_Sinker.py

  • +
  • [ ] Ex_Stokes_Sphere_SphCoords_WIP.py

    +
      +
    • [ ] UW evaluate issue inside plotting

    • +
    +
  • +
  • [x] Ex_Stokes_Spherical_FreeSlipBCs.py

  • +
  • [x] Ex_Stokes_Swarm_Bubbles.py

  • +
  • [x] Ex_Stokes_Swarm_RT_Cartesian.py

  • +
  • [x] Ex_stokes_sinkingBlock_benchmark.py

  • +
  • [x] ISSUE_Function_Evaluation_mem_leak.py

  • +
  • [x] Untitled.ipynb

  • +
  • [x] StokesIC_FreeSlip_DOCS.py

  • +
  • [x] Ex_WIP_Stokes_Periodic.py

  • +
  • [x] Ex_Stokes_Swarm_RT_Spherical.py

  • +
  • [x] Ex_Stokes_Cartesian_SolC.py

  • +
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-StokesFlow/output/README.html b/main/Notebooks/Examples-StokesFlow/output/README.html new file mode 100644 index 0000000..92aea98 --- /dev/null +++ b/main/Notebooks/Examples-StokesFlow/output/README.html @@ -0,0 +1,519 @@ + + + + + + + + + + + Convection model outputs — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Convection model outputs

+ +
+
+ +
+
+
+ + + + +
+ +
+

Convection model outputs#

+

These files are NOT under version control

+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Stokes_Kernels/Ex_Stokes_Annulus_Kernels.html b/main/Notebooks/Examples-Stokes_Kernels/Ex_Stokes_Annulus_Kernels.html new file mode 100644 index 0000000..f35993c --- /dev/null +++ b/main/Notebooks/Examples-Stokes_Kernels/Ex_Stokes_Annulus_Kernels.html @@ -0,0 +1,883 @@ + + + + + + + + + + + Cylindrical Stokes — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Cylindrical Stokes

+ +
+
+ +
+
+
+ + + + +
+ +
+

Cylindrical Stokes#

+

Mesh with embedded internal surface. This allows us to introduce an internal force integral

+
+
+
%%html
+
+<iframe width="750" 
+        height="500" 
+        src="./scene-export-9.html"
+        title="Annulus Kernel Model">
+</iframe>
+
+
+
+
+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+
+import underworld3 as uw
+from underworld3.systems import Stokes
+from underworld3 import function
+
+import numpy as np
+import sympy
+
+res = 0.05
+r_o = 1.0
+r_int = 0.8
+r_i = 0.5
+
+free_slip_upper = True
+free_slip_lower = True
+
+options = PETSc.Options()
+# options["help"] = None
+# options["pc_type"]  = "svd"
+# options["dm_plex_check_all"] = None
+
+import os
+
+os.environ["SYMPY_USE_CACHE"] = "no"
+
+
+
+
+
+
+
meshball = uw.meshing.AnnulusInternalBoundary(radiusOuter=r_o, 
+                                              radiusInternal=r_int, 
+                                              radiusInner=r_i, 
+                                              cellSize_Inner=res,
+                                              cellSize_Internal=res*0.5,
+                                              cellSize_Outer=res,
+                                              filename="tmp_fixedstarsMesh.msh")
+
+
+
+
+
+
+
norm_v = uw.discretisation.MeshVariable("N", meshball, 2, degree=1, varsymbol=r"{\hat{n}}")
+
+projection = uw.systems.Vector_Projection(meshball, norm_v)
+projection.uw_function = sympy.Matrix([[0,0]])
+projection.smoothing = 1.0e-3
+
+# Point in a consistent direction wrt vertical 
+GammaNorm = meshball.Gamma.dot(meshball.CoordinateSystem.unit_e_0) / sympy.sqrt(meshball.Gamma.dot(meshball.Gamma))
+
+projection.add_natural_bc(meshball.Gamma * GammaNorm, "Upper")
+projection.add_natural_bc(meshball.Gamma * GammaNorm, "Lower")
+projection.add_natural_bc(meshball.Gamma * GammaNorm, "Internal")
+
+projection.solve(verbose=False, debug=False)
+
+with meshball.access(norm_v):
+    norm_v.data[:,:] /= np.sqrt(norm_v.data[:,0]**2 + norm_v.data[:,1]**2).reshape(-1,1)
+
+
+
+
+
+
+
# Create a density structure / buoyancy force
+# gravity will vary linearly from zero at the centre
+# of the sphere to (say) 1 at the surface
+
+radius_fn = meshball.CoordinateSystem.xR[0]
+unit_rvec = meshball.CoordinateSystem.unit_e_0
+gravity_fn = 1  # radius_fn / r_o
+
+# Some useful coordinate stuff
+
+x, y = meshball.CoordinateSystem.X
+r, th = meshball.CoordinateSystem.xR
+
+# Null space in velocity (constant v_theta) expressed in x,y coordinates
+v_theta_fn_xy = r * meshball.CoordinateSystem.rRotN.T * sympy.Matrix((0,1))
+
+Rayleigh = 1.0e5
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable("V0", meshball, 2, degree=2, varsymbol=r"{v_0}")
+v_soln1 = uw.discretisation.MeshVariable("V1", meshball, 2, degree=2, varsymbol=r"{v_1}")
+p_soln = uw.discretisation.MeshVariable("p", meshball, 1, degree=1, continuous=True)
+p_cont = uw.discretisation.MeshVariable("pc", meshball, 1, degree=1, continuous=True)
+
+
+
+
+
+
+
# Create Stokes object
+
+stokes = Stokes(
+    meshball, velocityField=v_soln, pressureField=p_soln, solver_name="stokes"
+)
+
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.viscosity = 1.0
+stokes.penalty = 0.0
+stokes.saddle_preconditioner = sympy.simplify(1 / (stokes.constitutive_model.viscosity + stokes.penalty))
+
+stokes.petsc_options.setValue("ksp_monitor", None)
+stokes.petsc_options.setValue("snes_monitor", None)
+stokes.petsc_options["fieldsplit_velocity_mg_coarse_pc_type"] = "svd"
+
+t_init = sympy.sin(5*th) * sympy.exp(-1000.0 * ((r - r_int) ** 2)) 
+
+
+
+
+
+
+
## First solve with known normals
+
+stokes.bodyforce = sympy.Matrix([0,0])
+Gamma = meshball.CoordinateSystem.unit_e_0
+
+stokes.add_natural_bc(-t_init * unit_rvec, "Internal")
+
+if free_slip_upper:
+    stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) *  Gamma, "Upper")
+else:
+    stokes.add_essential_bc((0.0,0.0), "Upper")
+
+if free_slip_lower:
+    stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) *  Gamma, "Lower")
+else:
+    stokes.add_essential_bc((0.0,0.0), "Lower")
+
+stokes.solve()
+
+
+
+
+
+
+
# Null space evaluation
+
+I0 = uw.maths.Integral(meshball, v_theta_fn_xy.dot(v_soln.sym))
+norm = I0.evaluate()
+I0.fn = v_theta_fn_xy.dot(v_theta_fn_xy)
+vnorm = I0.evaluate()
+
+print(norm/vnorm, vnorm)
+
+with meshball.access(v_soln):
+    dv = uw.function.evaluate(norm * v_theta_fn_xy, v_soln.coords) / vnorm
+    v_soln.data[...] -= dv 
+
+with meshball.access(v_soln1):
+    v_soln1.data[...] = v_soln.data[...]
+
+
+
+
+
+
+
pressure_solver = uw.systems.Projection(meshball, p_cont)
+pressure_solver.uw_function = p_soln.sym[0]
+pressure_solver.smoothing = 1.0e-3
+
+
+
+
+
+
+
## Now solve with normals from nodal projection
+
+stokes._reset()
+
+stokes.bodyforce = sympy.Matrix([0,0])
+Gamma = meshball.Gamma / sympy.sqrt(meshball.Gamma.dot(meshball.Gamma))
+Gamma = norm_v.sym
+
+stokes.add_natural_bc(-t_init * unit_rvec, "Internal")
+
+if free_slip_upper:
+    stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) *  Gamma, "Upper")
+else:
+    stokes.add_essential_bc((0.0,0.0), "Upper")
+
+if free_slip_lower:
+    stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) *  Gamma, "Lower")
+else:
+    stokes.add_essential_bc((0.0,0.0), "Lower")
+
+stokes.solve(zero_init_guess=False)
+
+
+
+
+
+
+
# Null space evaluation
+
+I0 = uw.maths.Integral(meshball, v_theta_fn_xy.dot(v_soln.sym))
+norm = I0.evaluate()
+
+with meshball.access(v_soln):
+    dv = uw.function.evaluate(norm * v_theta_fn_xy, v_soln.coords) / vnorm
+    v_soln.data[...] -= dv 
+
+print(norm/vnorm, vnorm)
+# -9.662093930530614e-09 0.024291704747453444
+
+norm = I0.evaluate()
+
+
+
+
+
+
+
I0 = uw.maths.Integral(meshball, v_theta_fn_xy.dot(v_soln.sym))
+norm = I0.evaluate()
+print(norm/vnorm)
+
+
+
+
+
+
+
# Pressure at mesh nodes
+pressure_solver.solve()
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+
+if uw.mpi.size == 1:
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshball)
+    
+    velocity_points = vis.meshVariable_to_pv_cloud(v_soln)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym)
+    velocity_points.point_data["V0"] = vis.vector_fn_to_pv_points(velocity_points, v_soln1.sym)
+    velocity_points.point_data["dV"] = velocity_points.point_data["V"] - velocity_points.point_data["V0"]
+
+    pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_cont.sym)
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_init)
+    pvmesh.point_data["V0"] = vis.vector_fn_to_pv_points(pvmesh, v_soln1.sym)
+    pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)
+    pvmesh.point_data["dV"] = pvmesh.point_data["V"] - pvmesh.point_data["V0"]
+    pvmesh.point_data["Vmag"] = np.hypot(pvmesh.point_data["V"][:,0],pvmesh.point_data["V"][:,1])
+
+    skip = 3
+    points = np.zeros((meshball._centroids[::skip].shape[0], 3))
+    points[:, 0] = meshball._centroids[::skip, 0]
+    points[:, 1] = meshball._centroids[::skip, 1]
+    point_cloud = pv.PolyData(points)
+
+    pvstream = pvmesh.streamlines_from_source(
+        point_cloud, vectors="V", 
+        integration_direction="both", 
+        integrator_type=2,
+        surface_streamlines=True,
+        initial_step_length=0.01,
+        max_time=0.25,
+        max_steps=500
+    )
+   
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Grey",
+        scalars="Vmag",
+        show_edges=True,
+        use_transparency=False,
+        opacity=1.0,
+        show_scalar_bar=True
+    )
+
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=3)
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V0"], mag=3, color="Black")
+    # pl.add_arrows(velocity_points.points, velocity_points.point_data["dV"], mag=100, color="Black")
+    pl.add_mesh(pvstream, opacity=0.3, show_scalar_bar=False)
+
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
vsol_rms = np.sqrt(velocity_points.point_data["V"][:, 0] ** 2 + velocity_points.point_data["V"][:, 1] ** 2).mean()
+vsol_rms
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Stokes_Kernels/Ex_Stokes_Cartesian_Kernels.html b/main/Notebooks/Examples-Stokes_Kernels/Ex_Stokes_Cartesian_Kernels.html new file mode 100644 index 0000000..a987f2f --- /dev/null +++ b/main/Notebooks/Examples-Stokes_Kernels/Ex_Stokes_Cartesian_Kernels.html @@ -0,0 +1,875 @@ + + + + + + + + + + + Cartesian Stokes Kernels — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Cartesian Stokes Kernels

+ +
+
+ +
+
+
+ + + + +
+ +
+

Cartesian Stokes Kernels#

+

Mesh with embedded internal surface

+

This allows us to introduce an internal force integral

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+
+import underworld3 as uw
+from underworld3.systems import Stokes
+from underworld3 import function
+
+import numpy as np
+import sympy
+
+res = 0.05
+resH = 0.25
+
+options = PETSc.Options()
+
+import os
+
+os.environ["SYMPY_USE_CACHE"] = "no"
+
+
+
+
+
+
+
from enum import Enum
+
+class boundaries(Enum):
+    Top = 1
+    Bottom = 2
+    Internal = 3
+    Left = 4
+    Right = 5
+
+
+xmin = 0.0
+xmax = 1.0
+ymin = 0.0
+ymax = 1.0
+yint = 0.66
+
+cellSize = res
+
+if uw.mpi.rank == 0:
+    import gmsh
+
+    gmsh.initialize()
+    gmsh.option.setNumber("General.Verbosity", 0)
+    gmsh.model.add("KernelBox")
+
+    p1 = gmsh.model.geo.add_point(xmin, ymin, 0.0, meshSize=cellSize)
+    p2 = gmsh.model.geo.add_point(xmax, ymin, 0.0, meshSize=cellSize)
+    p3 = gmsh.model.geo.add_point(xmin, ymax, 0.0, meshSize=cellSize)
+    p4 = gmsh.model.geo.add_point(xmax, ymax, 0.0, meshSize=cellSize)
+
+    # Internal surface points
+    p5 = gmsh.model.geo.add_point(xmin, yint, 0.0, meshSize=cellSize)
+    p6 = gmsh.model.geo.add_point(xmax, yint, 0.0, meshSize=cellSize)
+
+    l1 = gmsh.model.geo.add_line(p1, p2)
+    l2 = gmsh.model.geo.add_line(p3, p4) 
+    l3 = gmsh.model.geo.add_line(p1, p5)
+    l4 = gmsh.model.geo.add_line(p5, p3)
+    l5 = gmsh.model.geo.add_line(p2, p6)
+    l6 = gmsh.model.geo.add_line(p6, p4) 
+    l7 = gmsh.model.geo.add_line(p5, p6)
+
+    cl1 = gmsh.model.geo.add_curve_loop((l1, l5, -l7, -l3))
+    cl2 = gmsh.model.geo.add_curve_loop((-l2, -l4, l7, l6))
+    
+    gmsh.model.geo.synchronize()
+
+    # gmsh.model.geo.add_curve_loops([cl1,cl2])
+    surface1 = gmsh.model.geo.add_plane_surface([cl1])
+    surface2 = gmsh.model.geo.add_plane_surface([cl2])
+
+    gmsh.model.geo.synchronize()
+
+    # Add Physical groups for boundaries
+    gmsh.model.add_physical_group(1, [l1,], boundaries.Bottom.value)
+    gmsh.model.set_physical_name(1, l1, boundaries.Bottom.name)
+    gmsh.model.add_physical_group(1, [l2], boundaries.Top.value)
+    gmsh.model.set_physical_name(1, l2, boundaries.Top.name)
+    gmsh.model.add_physical_group(1, [l3, l4], boundaries.Left.value)
+    gmsh.model.set_physical_name(1, l3, boundaries.Left.name)
+    gmsh.model.add_physical_group(1, [l5,l6], boundaries.Right.value)
+    gmsh.model.set_physical_name(1, l4, boundaries.Right.name)            
+    
+    gmsh.model.add_physical_group(1, [l7], boundaries.Internal.value)
+    gmsh.model.set_physical_name(1, l7, boundaries.Internal.name)
+
+    gmsh.model.addPhysicalGroup(2, [surface1,surface2], 99999)
+    gmsh.model.setPhysicalName(2, 99999, "Elements")
+
+    gmsh.model.geo.synchronize()
+
+    gmsh.model.mesh.generate(2)
+    gmsh.write("tmp_cart_kernel_mesh.msh")
+
+    # gmsh.fltk.run()
+
+    gmsh.finalize()
+
+kernel_mesh = uw.discretisation.Mesh(
+        "tmp_cart_kernel_mesh.msh",
+        degree=1,
+        qdegree=3,
+        useMultipleTags=True,
+        useRegions=False,
+        markVertices=True,
+        boundaries=boundaries,
+        coordinate_system_type=None,
+        refinement=0,
+        refinement_callback=None,
+        return_coords_to_bounds=None,
+    )
+
+x,y = kernel_mesh.X
+
+## The internal bc is not read correctly
+## We set useRegions=False and then unstack the boundaries
+## OK because we know (because it's our mesh) that the face-sets capture the boundaries 
+
+uw.adaptivity._dm_unstack_bcs(kernel_mesh.dm, kernel_mesh.boundaries, "Face Sets")
+
+kernel_mesh.dm.view()
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable(r"\mathbf{u}", kernel_mesh, 2, degree=2)
+p_soln = uw.discretisation.MeshVariable(r"p", kernel_mesh, 1, degree=1, continuous=False)
+p_cont = uw.discretisation.MeshVariable(r"pc", kernel_mesh, 1, degree=1, continuous=True)
+syy = uw.discretisation.MeshVariable(r"Syy", kernel_mesh, 1, degree=1, continuous=True)
+
+
+
+
+
+
+
if 0 and uw.mpi.size == 1:
+    
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(kernel_mesh)
+
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_mesh(pvmesh, "Grey", "wireframe")
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="Greens",
+        edge_color="Grey",
+        show_edges=True,
+        use_transparency=False,
+        clim=[0.66, 1],
+        opacity=0.75,
+    )
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
# Create Stokes object
+
+stokes = Stokes(
+    kernel_mesh, velocityField=v_soln, pressureField=p_soln, solver_name="stokes"
+)
+
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.viscosity = 1.0
+stokes.saddle_preconditioner = 1.0
+
+t_init = sympy.cos(3*x*sympy.pi) * sympy.exp(-1000.0 * ((y - yint) ** 2)) 
+
+stokes.add_essential_bc(sympy.Matrix([sympy.oo, 0.0]), "Top")
+stokes.add_essential_bc(sympy.Matrix([sympy.oo, 0.0]), "Bottom")
+stokes.add_essential_bc(sympy.Matrix([0.0,sympy.oo]), "Left")
+stokes.add_essential_bc(sympy.Matrix([0.0,sympy.oo]), "Right")
+
+stokes.add_natural_bc(sympy.Matrix([0.0, -t_init]), "Internal")
+
+stokes.bodyforce = sympy.Matrix([0,0])
+
+
+
+
+
+
+
pressure_solver = uw.systems.Projection(kernel_mesh, p_cont)
+pressure_solver.uw_function = p_soln.sym[0]
+pressure_solver.smoothing = 1.0e-3
+
+stress_solver = uw.systems.Projection(kernel_mesh, syy)
+stress_solver.uw_function = stokes.constitutive_model.flux[1,1]
+stress_solver.smoothing = 0.0e-6
+
+
+
+
+
+
+
stokes.petsc_options.setValue("ksp_monitor", None)
+stokes.petsc_options.setValue("snes_monitor", None)
+stokes.petsc_options.delValue("snes_converged_reason")
+stokes.solve()
+
+
+
+
+
+
+
# Pressure at mesh nodes
+pressure_solver.solve()
+stress_solver.solve()
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+
+if uw.mpi.size == 1:
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(kernel_mesh)
+    
+    velocity_points = vis.meshVariable_to_pv_cloud(v_soln)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym * sympy.exp(-100000.0 * ((y - 1) ** 2) ))
+    velocity_points.point_data["V"][:,0] = 0
+    velocity_points.point_data["Syy"] = vis.scalar_fn_to_pv_points(velocity_points, syy.sym)
+    velocity_points.point_data["SyyV"] = velocity_points.point_data["V"].copy() * 0.0
+    velocity_points.point_data["SyyV"][:,1] =  vis.scalar_fn_to_pv_points(velocity_points, syy.sym[0] * sympy.exp(-100000.0 * ((y - 1) ** 2)))
+
+
+    pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_cont.sym)
+    pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_init)
+    pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)
+
+    points = np.zeros((kernel_mesh._centroids.shape[0], 3))
+    points[:, 0] = kernel_mesh._centroids[:, 0]
+    points[:, 1] = kernel_mesh._centroids[:, 1]
+    point_cloud = pv.PolyData(points)
+
+    pvstream = pvmesh.streamlines_from_source(
+        point_cloud, vectors="V", 
+        integration_direction="both", 
+        integrator_type=2,
+        surface_streamlines=True,
+        initial_step_length=0.01,
+        max_time=1.0,
+        max_steps=500
+    )
+   
+    pl = pv.Plotter(title="Stokes Greens Functions", window_size=(750, 750))
+
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Grey",
+        scalars="P",
+        show_edges=True,
+        use_transparency=False,
+        opacity=1.0,
+        show_scalar_bar=False,
+    )
+
+    pl.add_mesh(pvstream, opacity=0.3, show_scalar_bar=False)
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["SyyV"], mag=-1, show_scalar_bar=False)
+
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
stokes.view()
+
+
+
+
+
+
+
stokes.constitutive_model.view()
+
+
+
+
+
+
+
stokes.constitutive_model.C
+
+
+
+
+
+
+
stokes.constitutive_model.c
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-SwarmAndParticles/CheckSwarmLayout.html b/main/Notebooks/Examples-SwarmAndParticles/CheckSwarmLayout.html new file mode 100644 index 0000000..baaf42e --- /dev/null +++ b/main/Notebooks/Examples-SwarmAndParticles/CheckSwarmLayout.html @@ -0,0 +1,672 @@ + + + + + + + + + + + <no title> — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+import underworld3 as uw
+from underworld3.swarm import SwarmPICLayout
+import numpy as np
+
+
+
+
+
+
+
n_els = 2
+mesh = uw.meshing.UnstructuredSimplexBox(
+    minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0), 
+    cellSize=1 / n_els, qdegree=2, refinement=0
+)
+mesh.dm.view()
+
+
+
+
+
+
+
v = uw.discretisation.MeshVariable("U", mesh, mesh.dim, degree=2)
+p = uw.discretisation.MeshVariable("P", mesh, 1, degree=1)
+
+
+
+
+
+
+
swarm = uw.swarm.Swarm(mesh=mesh)
+swarm.populate_petsc(
+    fill_param=2,
+    layout = SwarmPICLayout.GAUSS
+)
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+
+if uw.mpi.size == 1:
+
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh)
+
+    
+    # with mesh.access():
+    #     usol = v_soln.data.copy()
+
+    # with mesh.access():
+    #     pvmesh.point_data["Vmag"] = uw.function.evalf(
+    #         sympy.sqrt(v_soln.sym.dot(v_soln.sym)), mesh.data
+    #     )
+    #     pvmesh.point_data["P"] = uw.function.evalf(p_soln.fn, mesh.data)
+
+
+    # v_vectors = np.zeros((mesh.data.shape[0], 3))
+    # v_vectors[:, 0] = uw.function.evalf(v_soln[0].sym, mesh.data)
+    # v_vectors[:, 1] = uw.function.evalf(v_soln[1].sym, mesh.data)
+    # pvmesh.point_data["V"] = v_vectors
+
+    # arrow_loc = np.zeros((v_soln.coords.shape[0], 3))
+    # arrow_loc[:, 0:2] = v_soln.coords[...]
+
+    # arrow_length = np.zeros((v_soln.coords.shape[0], 3))
+    # arrow_length[:, 0:2] = usol[...]
+
+    # point sources at cell centres
+    points = np.zeros((mesh._centroids.shape[0], 3))
+    points[:, 0] = mesh._centroids[:, 0]
+    points[:, 1] = mesh._centroids[:, 1]
+    point_cloud = pv.PolyData(points)
+
+    spoints = vis.swarm_to_pv_cloud(swarm)
+    spoint_cloud = pv.PolyData(spoints)
+
+    pl = pv.Plotter(window_size=(1000, 750))
+
+    # pl.add_arrows(arrow_loc, arrow_length, mag=0.025 / U0, opacity=0.75)
+
+    pl.add_points(spoint_cloud,color="Black",
+                  render_points_as_spheres=False,
+                  point_size=5, opacity=0.66
+                )
+
+    pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.75)
+    # pl.add_mesh(pvstream)
+
+    # pl.remove_scalar_bar("mag")
+
+    pl.show()
+
+
+
+
+
+
+
with swarm.access():
+    print(swarm.data.shape)
+
+
+
+
+
+
+
14*6
+
+
+
+
+
+
+
mesh._centroids.shape
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-UW-2to3/Ex_stokes_Sinker-UWComp-Linear.html b/main/Notebooks/Examples-UW-2to3/Ex_stokes_Sinker-UWComp-Linear.html new file mode 100644 index 0000000..254b72c --- /dev/null +++ b/main/Notebooks/Examples-UW-2to3/Ex_stokes_Sinker-UWComp-Linear.html @@ -0,0 +1,1203 @@ + + + + + + + + + + + ‘‘Stokes Sinker’’ — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + +
+ +
+

‘‘Stokes Sinker’’#

+

Testing the Stokes sinker problem between UW2 and UW3. This system consists of a dense, high viscosity sphere falling through a background lower density and a viscous fluid.

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
# %%
+import underworld as uw2
+from underworld import function as fn
+import underworld.visualisation as vis
+import numpy as np
+import math
+
+
+
+
+
+
+
rank = uw2.mpi.rank
+
+
+
+
+
+

Setup parameters#

+

Set simulation parameters for the test and position of the spherical sinker.

+
+
+
# %%
+# Set the resolution.
+res = 32
+
+
+
+
+
+
+
# Set size and position of dense sphere.
+sphereRadius = 0.1
+sphereCentre = (0.0, 0.7)
+
+
+
+
+
+
+
# define some names for our index
+materialLightIndex = 0
+materialHeavyIndex = 1
+
+
+
+
+
+
+
# Set constants for the viscosity and density of the sinker.
+viscBG = 1.0
+viscSphere = 10.0
+
+
+
+
+
+
+
densityBG = 1.0
+densitySphere = 10.0
+
+
+
+
+
+
+
# location of tracer at bottom of sinker
+x_pos = sphereCentre[0]
+y_pos = sphereCentre[1] - sphereRadius
+
+
+
+
+
+
+
nsteps = 10
+
+
+
+
+
+
+
swarmGPC = 4
+
+
+
+
+
+
+

Create UW2 version#

+
+
+
# %%
+def uw2_stokesSinker():
+
+    mesh = uw2.mesh.FeMesh_Cartesian(
+        elementType=("Q1/dQ0"),
+        elementRes=(int(res * 2), int(res)),
+        minCoord=(-1.0, 0.0),
+        maxCoord=(1.0, 1.0),
+    )
+
+    velocityField = mesh.add_variable(nodeDofCount=2)
+    pressureField = mesh.subMesh.add_variable(nodeDofCount=1)
+
+    velocityField.data[:] = [0.0, 0.0]
+    pressureField.data[:] = 0.0
+
+    # Create the swarm and an advector associated with it
+    swarm = uw2.swarm.Swarm(mesh=mesh)
+    advector = uw2.systems.SwarmAdvector(
+        swarm=swarm, velocityField=velocityField, order=2
+    )
+
+    # Add a data variable which will store an index to determine material.
+    materialIndex = swarm.add_variable(dataType="int", count=1)
+
+    # Create a layout object that will populate the swarm across the whole domain.
+    swarmLayout = uw2.swarm.layouts.PerCellGaussLayout(
+        swarm=swarm, gaussPointCount=swarmGPC
+    )
+    # swarmLayout = uw2.swarm.layouts.PerCellSpaceFillerLayout( swarm=swarm, particlesPerCell=swarmFill )
+
+    # Go ahead and populate the swarm.
+    swarm.populate_using_layout(layout=swarmLayout)
+
+    # create a function for a sphere. returns `True` if query is inside sphere, `False` otherwise.
+    coord = fn.input() - sphereCentre
+    fn_sphere = fn.math.dot(coord, coord) < sphereRadius**2
+
+    # set up the condition for being in a sphere. If not in sphere then will return light index.
+    conditions = [(fn_sphere, materialHeavyIndex), (True, materialLightIndex)]
+
+    # Execute the branching conditional function, evaluating at the location of each particle in the swarm.
+    # The results are copied into the materialIndex swarm variable.
+    materialIndex.data[:] = fn.branching.conditional(conditions).evaluate(swarm)
+
+    # build a tracer swarm with one particle
+    tracerSwarm = uw2.swarm.Swarm(mesh)
+    advector_tracer = uw2.systems.SwarmAdvector(
+        swarm=tracerSwarm, velocityField=velocityField, order=2
+    )
+
+    # build a numpy array with one particle, specifying it's exact location
+    coord_array = np.array(object=(x_pos, y_pos), ndmin=2)
+    tracerSwarm.add_particles_with_coordinates(coord_array)
+
+    tracer = numpy.zeros(shape=(1, 2))
+    tracer[:, 0], tracer[:, 1] = x_pos, y_pos
+
+    fig1 = vis.Figure(figsize=(800, 400))
+    fig1.Points(swarm, materialIndex, colourBar=False, pointSize=2.0)
+    fig1.VectorArrows(mesh, velocityField)
+    fig1.show()
+
+    # Here we set a viscosity value of '1.' for both materials
+    mappingDictViscosity = {materialLightIndex: viscBG, materialHeavyIndex: viscSphere}
+    # Create the viscosity map function.
+    viscosityMapFn = fn.branching.map(
+        fn_key=materialIndex, mapping=mappingDictViscosity
+    )
+    # Here we set a density of '0.' for the lightMaterial, and '1.' for the heavymaterial.
+    mappingDictDensity = {
+        materialLightIndex: densityBG,
+        materialHeavyIndex: densitySphere,
+    }
+    # Create the density map function.
+    densityFn = fn.branching.map(fn_key=materialIndex, mapping=mappingDictDensity)
+
+    # And the final buoyancy force function.
+    z_hat = (0.0, 1.0)
+    buoyancyFn = -densityFn * z_hat
+
+    iWalls = mesh.specialSets["MinI_VertexSet"] + mesh.specialSets["MaxI_VertexSet"]
+    jWalls = mesh.specialSets["MinJ_VertexSet"] + mesh.specialSets["MaxJ_VertexSet"]
+
+    freeslipBC = uw2.conditions.DirichletCondition(
+        variable=velocityField, indexSetsPerDof=(iWalls, jWalls)
+    )
+
+    noslipBC = uw2.conditions.DirichletCondition(
+        variable=velocityField, indexSetsPerDof=(iWalls + jWalls, iWalls + jWalls)
+    )
+
+    stokes = uw2.systems.Stokes(
+        velocityField=velocityField,
+        pressureField=pressureField,
+        voronoi_swarm=swarm,
+        conditions=noslipBC,
+        fn_viscosity=viscosityMapFn,
+        fn_bodyforce=buoyancyFn,
+    )
+
+    solver = uw2.systems.Solver(stokes)
+
+    top = mesh.specialSets["MaxJ_VertexSet"]
+    surfaceArea = uw2.utils.Integral(
+        fn=1.0, mesh=mesh, integrationType="surface", surfaceIndexSet=top
+    )
+    surfacePressureIntegral = uw2.utils.Integral(
+        fn=pressureField, mesh=mesh, integrationType="surface", surfaceIndexSet=top
+    )
+
+    # a callback function to calibrate the pressure - will pass to solver later
+    def pressure_calibrate():
+        (area,) = surfaceArea.evaluate()
+        (p0,) = surfacePressureIntegral.evaluate()
+        offset = p0 / area
+        if rank == 0:
+            print(
+                "Zeroing pressure using mean upper surface pressure {}".format(offset)
+            )
+        pressureField.data[:] -= offset
+
+    vdotv = fn.math.dot(velocityField, velocityField)
+
+    # Stepping. Initialise time and timestep.
+    time = 0.0
+    step = 0
+
+    tSinker = np.zeros(nsteps)
+    ySinker0 = np.zeros(nsteps)
+
+    ySinker1 = np.zeros(nsteps)
+
+    # Perform 10 steps
+    while step < nsteps:
+        # Get velocity solution - using callback
+        solver.solve()
+        # Calculate the RMS velocity
+        vrms = math.sqrt(mesh.integrate(vdotv)[0] / mesh.integrate(1.0)[0])
+
+        if rank == 0:
+            ymin0 = np.copy(tracerSwarm.data[:, 1].min())
+            ymin1 = np.copy(tracer[:, 1].min())
+
+            ySinker0[step] = ymin0
+            ySinker1[step] = ymin1
+
+            tSinker[step] = time
+
+            print(
+                "step = {0:6d}; time = {1:.3e}; v_rms = {2:.3e}; height0 = {3:.3e}; height1 = {3:.3e}".format(
+                    step, time, vrms, ymin0, ymin1
+                )
+            )
+
+        # Retrieve the maximum possible timestep for the advection system.
+        dt = advector.get_max_dt()
+        # Advect using this timestep size.
+        advector.integrate(dt)
+        advector_tracer.integrate(dt)
+
+        vel_on_tracer = velocityField.evaluate(tracer)
+        tracer += dt * vel_on_tracer
+
+        step += 1
+        time += dt
+
+    if rank == 0:
+        print(
+            "Initial position: t = {0:.3f}, y = {1:.3f}".format(tSinker[0], ySinker0[0])
+        )
+        print(
+            "Final position:   t = {0:.3f}, y = {1:.3f}".format(
+                tSinker[nsteps - 1], ySinker0[nsteps - 1]
+            )
+        )
+
+        uw2.utils.matplotlib_inline()
+        import matplotlib.pyplot as pyplot
+
+        fig = pyplot.figure()
+        fig.set_size_inches(12, 6)
+        ax = fig.add_subplot(1, 1, 1)
+        ax.plot(tSinker, ySinker0)
+        ax.plot(tSinker, ySinker1)
+        ax.set_xlabel("Time")
+        ax.set_ylabel("Sinker position")
+
+        fig1.show()
+
+        return tSinker, ySinker0, ySinker1
+
+
+
+
+
+
+

Create UW3 version#

+
+
+
# %%
+from petsc4py import PETSc
+import underworld3 as uw3
+from underworld3.systems import Stokes
+import numpy
+import sympy
+from mpi4py import MPI
+
+
+
+
+
+
+
options = PETSc.Options()
+
+
+
+
+
+
+
# %%
+def uw3_stokesSinker(render=True):
+
+    sys = PETSc.Sys()
+    sys.pushErrorHandler("traceback")
+
+    options = PETSc.Options()
+    options["snes_converged_reason"] = None
+    options["snes_monitor_short"] = None
+
+    mesh = uw3.meshing.StructuredQuadBox(
+        elementRes=(int(res), int(res)), minCoords=(-1.0, 0.0), maxCoords=(1.0, 1.0)
+    )
+
+    v = uw3.discretisation.MeshVariable("U", mesh, mesh.dim, degree=2)
+    p = uw3.discretisation.MeshVariable("P", mesh, 1, degree=1)
+
+    stokes = uw3.systems.Stokes(mesh, velocityField=v, pressureField=p)
+
+    ### free slip.
+    ### note with petsc we always need to provide a vector of correct cardinality.
+    # stokes.add_dirichlet_bc( (0.,0.), ["Bottom",  "Top"], 1 )  # top/bottom: components, function, markers
+    # stokes.add_dirichlet_bc( (0.,0.), ["Left", "Right"],  0 )  # left/right: components, function, markers
+
+    ### No slip (?)
+    sol_vel = sympy.Matrix([0, 0])
+
+    stokes.add_dirichlet_bc(
+        sol_vel, ["Top", "Bottom"], [0, 1]
+    )  # top/bottom: components, function, markers
+    stokes.add_dirichlet_bc(
+        sol_vel, ["Left", "Right"], [0, 1]
+    )  # left/right: components, function, markers
+
+    swarm = uw3.swarm.Swarm(mesh=mesh)
+    material = uw3.swarm.IndexSwarmVariable("M", swarm, indices=4)
+    swarm.populate(fill_param=swarmGPC)
+
+    blob = numpy.array(
+        # [[ 0.25, 0.75, 0.1,  1],
+        #  [ 0.45, 0.70, 0.05, 2],
+        #  [ 0.65, 0.60, 0.06, 3],
+        [[sphereCentre[0], sphereCentre[1], sphereRadius, 1]]
+    )
+    # [ 0.65, 0.20, 0.06, 2],
+    # [ 0.45, 0.20, 0.12, 3] ])
+
+    with swarm.access(material):
+        material.data[...] = materialLightIndex
+
+        for i in range(blob.shape[0]):
+            cx, cy, r, m = blob[i, :]
+            inside = (swarm.data[:, 0] - cx) ** 2 + (
+                swarm.data[:, 1] - cy
+            ) ** 2 < r**2
+            material.data[inside] = materialHeavyIndex
+
+    tracer = numpy.zeros(shape=(1, 2))
+    tracer[:, 0], tracer[:, 1] = x_pos, y_pos
+
+    mat_density = numpy.array([densityBG, densitySphere])
+
+    density = mat_density[0] * material.sym[0] + mat_density[1] * material.sym[1]
+
+    mat_viscosity = numpy.array([viscBG, viscSphere])
+
+    viscosity = mat_viscosity[0] * material.sym[0] + mat_viscosity[1] * material.sym[1]
+
+    def plot_fig():
+
+        import numpy as np
+        import pyvista as pv
+        import vtk
+
+        pv.global_theme.background = "white"
+        pv.global_theme.window_size = [750, 750]
+        pv.global_theme.antialiasing = True
+        pv.global_theme.jupyter_backend = "trame"
+        pv.global_theme.smooth_shading = True
+
+        mesh.vtk("tempMsh.vtk")
+        pvmesh = pv.read("tempMsh.vtk")
+
+        with swarm.access():
+            points = numpy.zeros((swarm.data.shape[0], 3))
+            points[:, 0] = swarm.data[:, 0]
+            points[:, 1] = swarm.data[:, 1]
+            points[:, 2] = 0.0
+
+        point_cloud = pv.PolyData(points)
+
+        with swarm.access():
+            point_cloud.point_data["M"] = material.data.copy()
+
+        pl = pv.Plotter(notebook=True)
+
+        pl.add_mesh(pvmesh, "Black", "wireframe")
+
+        # pl.add_points(point_cloud, color="Black",
+        #                   render_points_as_spheres=False,
+        #                   point_size=2.5, opacity=0.75)
+
+        pl.add_mesh(
+            point_cloud,
+            cmap="coolwarm",
+            edge_color="Black",
+            show_edges=False,
+            scalars="M",
+            use_transparency=False,
+            opacity=0.95,
+        )
+
+        pl.show(cpos="xy")
+
+    if render:
+        plot_fig()
+
+    stokes.constitutive_model = uw3.constitutive_models.ViscousFlowModel
+    stokes.constitutive_model.material_properties = (
+        stokes.constitutive_model.Parameters(viscosity=viscosity)
+    )
+
+    # stokes.viscosity = viscosity
+
+    stokes.bodyforce = -1.0 * density * mesh.N.j
+
+    step = 0
+    time = 0.0
+
+    tSinker = np.zeros(nsteps)
+    ySinker = np.zeros(nsteps)
+
+    while step < nsteps:
+
+        ### solve stokes
+        stokes.solve()
+
+        if MPI.COMM_WORLD.rank == 0:
+            ymin = tracer[:, 1].min()
+            ySinker[step] = ymin
+            tSinker[step] = time
+            print(
+                f"Step: {str(step).rjust(3)}, time: {time:6.2f}, tracer:  {ymin:6.2f}"
+            )  # , vrms {vrms_val:.3e}")
+
+        ### estimate dt
+        dt = stokes.estimate_dt()
+
+        with swarm.access():
+            vel_on_particles = uw3.function.evaluate(
+                stokes.u.fn, swarm.particle_coordinates.data
+            )
+
+        ### advect swarm
+        with swarm.access(swarm.particle_coordinates):
+            swarm.particle_coordinates.data[:] += dt * vel_on_particles
+
+        vel_on_tracer = uw3.function.evaluate(stokes.u.fn, tracer)
+        tracer += dt * vel_on_tracer
+
+        # if MPI.COMM_WORLD.rank==0:
+        # print('step = {0:6d}; time = {1:.3e}; v_rms = {2:.3e}; height = {3:.3e}'
+        #       .format(step,time,vrms,ymin))
+
+        step += 1
+        time += dt
+
+    if rank == 0:
+        print(
+            "Initial position: t = {0:.3f}, y = {1:.3f}".format(tSinker[0], ySinker[0])
+        )
+        print(
+            "Final position:   t = {0:.3f}, y = {1:.3f}".format(
+                tSinker[nsteps - 1], ySinker[nsteps - 1]
+            )
+        )
+
+        uw2.utils.matplotlib_inline()
+        import matplotlib.pyplot as pyplot
+
+        fig = pyplot.figure()
+        fig.set_size_inches(12, 6)
+        ax = fig.add_subplot(1, 1, 1)
+        ax.plot(tSinker, ySinker)
+        ax.set_xlabel("Time")
+        ax.set_ylabel("Sinker position")
+
+        # fig1.show()
+
+        if render:
+            plot_fig()
+
+        return tSinker, ySinker
+
+
+
+
+
+
+
# %%
+tSinker_UW2, ySinker0_UW2, ySinker1_UW2 = uw2_stokesSinker()
+
+
+
+
+
+
+
# %%
+tSinker_UW3, ySinker_UW3 = uw3_stokesSinker()
+
+
+
+
+
+
+

Compare velocity of tracer in both models to terminal velocity estimation#

+
+

Terminal velocity estimation is closest to model with no slip boundaries, free slip results in a higher velocity#

+
+
+
+

UW2 and UW3 velocities match when the viscosities in UW3 are doubled or densities are halved#

+
+
+
stokes_vel0 = (-1 * (2 * sphereRadius) ** 2 * (densitySphere - densityBG)) / (
+    18 * viscBG
+)
+stokes_vel1 = -1 * (2 / 9) * ((densitySphere - densityBG) / viscBG) * sphereRadius**2
+
+
+
+
+

print(f’stokes vel0: {stokes_vel0}, stokes vel1: {stokes_vel1}’)

+
+
+
time = np.arange(0, 7.5, 0.1)
+terminalStokes = stokes_vel0 * time
+
+
+
+
+
+
+
UW2_vel = (ySinker0_UW2[0] - ySinker0_UW2[-1]) / (tSinker_UW2[0] - tSinker_UW2[-1])
+UW3_vel = (ySinker_UW3[0] - ySinker_UW3[-1]) / (tSinker_UW3[0] - tSinker_UW3[-1])
+
+
+
+
+
+
+
# %%
+print(
+    f"\n\n\n alaytical velocity: {stokes_vel0}, UW2 velocity: {UW2_vel}, UW3 velocity: {UW3_vel/2.} \n\n\n"
+)
+
+
+
+
+
+
+
# %%
+if rank == 0:
+    import matplotlib.pyplot as pyplot
+
+    fig = pyplot.figure()
+    fig.set_size_inches(12, 6)
+    ax = fig.add_subplot(1, 1, 1)
+    ax.plot(tSinker_UW2, ySinker0_UW2, c="blue", ls="--", label="UW2")
+
+    ax.plot(tSinker_UW2, ySinker1_UW2, c="orange", ls="-.", label="UW2")
+
+    ax.plot(tSinker_UW3, ySinker_UW3, c="red", ls=":", label="UW3")
+
+    ax.plot(
+        time,
+        (sphereCentre[1] - sphereRadius) + terminalStokes,
+        c="k",
+        ls=":",
+        label="Terminal velocity",
+    )
+
+    ax.legend()
+
+    ax.set_xlabel("Time")
+    ax.set_ylabel("Sinker position")
+
+    # fig1.show()
+
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-UW-2to3/Ex_stokes_Sinker-UWComp-NonLinear.html b/main/Notebooks/Examples-UW-2to3/Ex_stokes_Sinker-UWComp-NonLinear.html new file mode 100644 index 0000000..ff996e7 --- /dev/null +++ b/main/Notebooks/Examples-UW-2to3/Ex_stokes_Sinker-UWComp-NonLinear.html @@ -0,0 +1,1214 @@ + + + + + + + + + + + ‘‘Non-linear Stokes Sinker’’ — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

‘‘Non-linear Stokes Sinker’’

+ +
+ +
+
+ + + + +
+ +
+

‘‘Non-linear Stokes Sinker’’#

+

Testing a non-linear implementation of the Stokes sinker between UW2 and UW3. This system consists of a dense, high viscosity sphere falling through a background lower density and a non-linear viscoplastic fluid (strain-rate dependent).

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
# %%
+import underworld as uw2
+from underworld import function as fn
+import underworld.visualisation as vis
+import numpy as np
+import math
+
+
+
+
+
+

Setup parameters#

+

Set simulation parameters for the test and position of the spherical sinker.

+
+
+
# %%
+# Set the resolution.
+res = 42
+
+
+
+
+
+
+
# Set size and position of dense sphere.
+sphereRadius = 0.1
+sphereCentre = (0.0, 0.7)
+
+
+
+
+
+
+
# define some names for our index
+materialLightIndex = 0
+materialHeavyIndex = 1
+
+
+
+
+
+
+
eta_min = 1e-2
+eta_max = 1e2
+
+
+
+
+
+
+
# Set constants for the viscosity and density of the sinker.
+viscBG = eta_max
+viscSphere = eta_max
+
+
+
+
+
+
+
densityBG = 1.0
+densitySphere = 10.0
+
+
+
+
+
+
+
Cohesion = 0.15
+
+
+
+
+
+
+
# location of tracer at bottom of sinker
+x_pos = sphereCentre[0]
+y_pos = sphereCentre[1] - sphereRadius
+
+
+
+
+
+
+
nsteps = 10
+
+
+
+
+
+
+
swarmGPC = 2
+
+
+
+
+
+
+

Create mesh and finite element variables#

+
+
+
# %%
+def uw2_stokesSinker():
+
+    mesh = uw2.mesh.FeMesh_Cartesian(
+        elementType=("Q1/dQ0"),
+        elementRes=(int(res), int(res)),
+        minCoord=(-1.0, 0.0),
+        maxCoord=(1.0, 1.0),
+    )
+
+    velocityField = mesh.add_variable(nodeDofCount=2)
+    pressureField = mesh.subMesh.add_variable(nodeDofCount=1)
+
+    velocityField.data[:] = [0.0, 0.0]
+    pressureField.data[:] = 0.0
+
+    # Create the swarm and an advector associated with it
+    swarm = uw2.swarm.Swarm(mesh=mesh)
+    advector = uw2.systems.SwarmAdvector(
+        swarm=swarm, velocityField=velocityField, order=2
+    )
+
+    # Add a data variable which will store an index to determine material.
+    materialIndex = swarm.add_variable(dataType="int", count=1)
+
+    # Create a layout object that will populate the swarm across the whole domain.
+    swarmLayout = uw2.swarm.layouts.PerCellGaussLayout(
+        swarm=swarm, gaussPointCount=swarmGPC
+    )
+    # swarmLayout = uw2.swarm.layouts.PerCellSpaceFillerLayout( swarm=swarm, particlesPerCell=swarmFill )
+
+    # Go ahead and populate the swarm.
+    swarm.populate_using_layout(layout=swarmLayout)
+
+    # create a function for a sphere. returns `True` if query is inside sphere, `False` otherwise.
+    coord = fn.input() - sphereCentre
+    fn_sphere = fn.math.dot(coord, coord) < sphereRadius**2
+
+    # set up the condition for being in a sphere. If not in sphere then will return light index.
+    conditions = [(fn_sphere, materialHeavyIndex), (True, materialLightIndex)]
+
+    # Execute the branching conditional function, evaluating at the location of each particle in the swarm.
+    # The results are copied into the materialIndex swarm variable.
+    materialIndex.data[:] = fn.branching.conditional(conditions).evaluate(swarm)
+
+    # build a tracer swarm with one particle
+    tracerSwarm = uw2.swarm.Swarm(mesh)
+    advector_tracer = uw2.systems.SwarmAdvector(
+        swarm=tracerSwarm, velocityField=velocityField, order=2
+    )
+
+    # build a numpy array with one particle, specifying it's exact location
+    coord_array = np.array(object=(x_pos, y_pos), ndmin=2)
+    tracerSwarm.add_particles_with_coordinates(coord_array)
+
+    tracer = numpy.zeros(shape=(1, 2))
+    tracer[:, 0], tracer[:, 1] = x_pos, y_pos
+
+    fig1 = vis.Figure(figsize=(800, 400))
+    fig1.Points(swarm, materialIndex, colourBar=False, pointSize=2.0)
+    fig1.VectorArrows(mesh, velocityField)
+    fig1.show()
+
+    # The yeilding of the BG material is dependent on the SR.
+    strainRate_2ndInvariant = fn.tensor.second_invariant(
+        fn.tensor.symmetric(velocityField.fn_gradient)
+    )
+
+    # vonMises = (Cohesion_BG / (2.*((strainRate_2ndInvariant+1.0e-20)*ref_SR))) / ref_viscosity
+
+    vonMises = Cohesion / (2.0 * ((strainRate_2ndInvariant + 1.0e-20)))
+
+    viscoplasticBG = fn.exception.SafeMaths(
+        fn.misc.min(fn.misc.max(fn.misc.min(vonMises, viscBG), eta_min), eta_max)
+    )
+
+    # Here we set a viscosity value of '1.' for both materials
+    mappingDictViscosity = {
+        materialLightIndex: viscoplasticBG,
+        materialHeavyIndex: viscSphere,
+    }
+    # Create the viscosity map function.
+    viscosityMapFn = fn.branching.map(
+        fn_key=materialIndex, mapping=mappingDictViscosity
+    )
+    # Here we set a density of '0.' for the lightMaterial, and '1.' for the heavymaterial.
+    mappingDictDensity = {
+        materialLightIndex: densityBG,
+        materialHeavyIndex: densitySphere,
+    }
+    # Create the density map function.
+    densityFn = fn.branching.map(fn_key=materialIndex, mapping=mappingDictDensity)
+
+    # And the final buoyancy force function.
+    z_hat = (0.0, 1.0)
+    buoyancyFn = -densityFn * z_hat
+
+    iWalls = mesh.specialSets["MinI_VertexSet"] + mesh.specialSets["MaxI_VertexSet"]
+    jWalls = mesh.specialSets["MinJ_VertexSet"] + mesh.specialSets["MaxJ_VertexSet"]
+
+    freeslipBC = uw2.conditions.DirichletCondition(
+        variable=velocityField, indexSetsPerDof=(iWalls, jWalls)
+    )
+
+    noslipBC = uw2.conditions.DirichletCondition(
+        variable=velocityField, indexSetsPerDof=(iWalls + jWalls, iWalls + jWalls)
+    )
+
+    stokes = uw2.systems.Stokes(
+        velocityField=velocityField,
+        pressureField=pressureField,
+        voronoi_swarm=swarm,
+        conditions=noslipBC,
+        fn_viscosity=viscosityMapFn,
+        fn_bodyforce=buoyancyFn,
+    )
+
+    solver = uw2.systems.Solver(stokes)
+    # solver.set_inner_method("lu")
+    # solver.set_inner_rtol(inner_rtol)
+    # solver.set_outer_rtol(10*inner_rtol)
+    # Optional solver settings
+
+    top = mesh.specialSets["MaxJ_VertexSet"]
+    surfaceArea = uw2.utils.Integral(
+        fn=1.0, mesh=mesh, integrationType="surface", surfaceIndexSet=top
+    )
+    surfacePressureIntegral = uw2.utils.Integral(
+        fn=pressureField, mesh=mesh, integrationType="surface", surfaceIndexSet=top
+    )
+
+    # a callback function to calibrate the pressure - will pass to solver later
+    def pressure_calibrate():
+        (area,) = surfaceArea.evaluate()
+        (p0,) = surfacePressureIntegral.evaluate()
+        offset = p0 / area
+        if rank == 0:
+            print(
+                "Zeroing pressure using mean upper surface pressure {}".format(offset)
+            )
+        pressureField.data[:] -= offset
+
+    vdotv = fn.math.dot(velocityField, velocityField)
+
+    # Stepping. Initialise time and timestep.
+    time = 0.0
+    step = 0
+
+    tSinker = np.zeros(nsteps)
+    ySinker0 = np.zeros(nsteps)
+
+    ySinker1 = np.zeros(nsteps)
+
+    # Perform 10 steps
+    while step < nsteps:
+        # Get velocity solution - using callback
+        solver.solve(nonLinearIterate=True)
+        # Calculate the RMS velocity
+        vrms = math.sqrt(mesh.integrate(vdotv)[0] / mesh.integrate(1.0)[0])
+
+        if uw2.mpi.rank == 0:
+            ymin0 = np.copy(tracerSwarm.data[:, 1].min())
+            ymin1 = np.copy(tracer[:, 1].min())
+
+            ySinker0[step] = ymin0
+            ySinker1[step] = ymin1
+
+            tSinker[step] = time
+
+            print(
+                "step = {0:6d}; time = {1:.3e}; v_rms = {2:.3e}; height0 = {3:.3e}; height1 = {3:.3e}".format(
+                    step, time, vrms, ymin0, ymin1
+                )
+            )
+
+        # Retrieve the maximum possible timestep for the advection system.
+        dt = advector.get_max_dt()
+        # Advect using this timestep size.
+        advector.integrate(dt)
+        advector_tracer.integrate(dt)
+
+        vel_on_tracer = velocityField.evaluate(tracer)
+        tracer += dt * vel_on_tracer
+
+        step += 1
+        time += dt
+
+    if uw2.mpi.rank == 0:
+        print(
+            "Initial position: t = {0:.3f}, y = {1:.3f}".format(tSinker[0], ySinker0[0])
+        )
+        print(
+            "Final position:   t = {0:.3f}, y = {1:.3f}".format(
+                tSinker[nsteps - 1], ySinker0[nsteps - 1]
+            )
+        )
+
+        uw2.utils.matplotlib_inline()
+        import matplotlib.pyplot as pyplot
+
+        fig = pyplot.figure()
+        fig.set_size_inches(12, 6)
+        ax = fig.add_subplot(1, 1, 1)
+        ax.plot(tSinker, ySinker0)
+        ax.plot(tSinker, ySinker1)
+        ax.set_xlabel("Time")
+        ax.set_ylabel("Sinker position")
+
+        fig1.show()
+
+        return tSinker, ySinker0, ySinker1
+
+
+
+
+
+
+
# %%
+from petsc4py import PETSc
+import underworld3 as uw3
+from underworld3.systems import Stokes
+import numpy
+import sympy
+from mpi4py import MPI
+
+
+
+
+
+
+
options = PETSc.Options()
+
+
+
+
+
+
+
# %%
+def uw3_stokesSinker(render=True):
+
+    sys = PETSc.Sys()
+    sys.pushErrorHandler("traceback")
+
+    options = PETSc.Options()
+    # options["ksp_rtol"] =  inner_rtol
+    # options["ksp_atol"] =  inner_rtol
+    options["snes_converged_reason"] = None
+    options["snes_monitor_short"] = None
+
+    mesh = uw3.meshing.StructuredQuadBox(
+        elementRes=(int(res), int(res)), minCoords=(-1.0, 0.0), maxCoords=(1.0, 1.0)
+    )
+
+    v = uw3.discretisation.MeshVariable("U", mesh, mesh.dim, degree=2)
+    p = uw3.discretisation.MeshVariable("P", mesh, 1, degree=1)
+
+    stokes = uw3.systems.Stokes(mesh, velocityField=v, pressureField=p)
+
+    stokes.constitutive_model = uw3.constitutive_models.ViscousFlowModel
+
+    ### free slip.
+    ### note with petsc we always need to provide a vector of correct cardinality.
+    # stokes.add_dirichlet_bc( (0.,0.), ["Bottom",  "Top"], 1 )  # top/bottom: components, function, markers
+    # stokes.add_dirichlet_bc( (0.,0.), ["Left", "Right"],  0 )  # left/right: components, function, markers
+
+    ### No slip (?)
+    sol_vel = sympy.Matrix([0, 0])
+
+    stokes.add_dirichlet_bc(
+        sol_vel, ["Top", "Bottom"], [0, 1]
+    )  # top/bottom: components, function, markers
+    stokes.add_dirichlet_bc(
+        sol_vel, ["Left", "Right"], [0, 1]
+    )  # left/right: components, function, markers
+
+    swarm = uw3.swarm.Swarm(mesh=mesh)
+    material = uw3.swarm.IndexSwarmVariable("M", swarm, indices=4)
+    swarm.populate(fill_param=swarmGPC)
+
+    blob = numpy.array(
+        # [[ 0.25, 0.75, 0.1,  1],
+        #  [ 0.45, 0.70, 0.05, 2],
+        #  [ 0.65, 0.60, 0.06, 3],
+        [[sphereCentre[0], sphereCentre[1], sphereRadius, 1]]
+    )
+    # [ 0.65, 0.20, 0.06, 2],
+    # [ 0.45, 0.20, 0.12, 3] ])
+
+    with swarm.access(material):
+        material.data[...] = 0
+
+        for i in range(blob.shape[0]):
+            cx, cy, r, m = blob[i, :]
+            inside = (swarm.data[:, 0] - cx) ** 2 + (
+                swarm.data[:, 1] - cy
+            ) ** 2 < r**2
+            material.data[inside] = m
+
+    tracer = numpy.zeros(shape=(1, 2))
+    tracer[:, 0], tracer[:, 1] = x_pos, y_pos
+
+    mat_density = numpy.array([densityBG, densitySphere])
+
+    density = mat_density[0] * material.sym[0] + mat_density[1] * material.sym[1]
+
+    viscoPlastic_BG = sympy.Min(Cohesion / (2.0 * (stokes._Einv2)), viscBG)
+
+    mat_viscosity = np.array([viscoPlastic_BG, viscSphere])
+
+    viscosityMat = (
+        mat_viscosity[0] * material.sym[0] + mat_viscosity[1] * material.sym[1]
+    )
+
+    viscosity = sympy.Max(sympy.Min(viscosityMat, eta_max), eta_min)
+
+    def plot_fig():
+
+        import numpy as np
+        import pyvista as pv
+        import vtk
+
+        pv.global_theme.background = "white"
+        pv.global_theme.window_size = [750, 750]
+        pv.global_theme.antialiasing = True
+        pv.global_theme.jupyter_backend = "trame"
+        pv.global_theme.smooth_shading = True
+
+        mesh.vtk("tempMsh.vtk")
+        pvmesh = pv.read("tempMsh.vtk")
+
+        with swarm.access():
+            points = numpy.zeros((swarm.data.shape[0], 3))
+            points[:, 0] = swarm.data[:, 0]
+            points[:, 1] = swarm.data[:, 1]
+            points[:, 2] = 0.0
+
+        point_cloud = pv.PolyData(points)
+
+        with swarm.access():
+            point_cloud.point_data["M"] = material.data.copy()
+
+        with mesh.access():
+            vsol = v.data.copy()
+
+        arrow_loc = np.zeros((v.coords.shape[0], 3))
+        arrow_loc[:, 0:2] = v.coords[...]
+
+        arrow_length = np.zeros((v.coords.shape[0], 3))
+        arrow_length[:, 0:2] = vsol[...]
+
+        pl = pv.Plotter(notebook=True)
+
+        pl.add_mesh(pvmesh, "Black", "wireframe")
+
+        # pl.add_points(point_cloud, color="Black",
+        #                   render_points_as_spheres=False,
+        #                   point_size=2.5, opacity=0.75)
+
+        pl.add_mesh(
+            point_cloud,
+            cmap="coolwarm",
+            edge_color="Black",
+            show_edges=False,
+            scalars="M",
+            use_transparency=False,
+            opacity=0.95,
+        )
+
+        pl.add_arrows(arrow_loc, arrow_length, mag=5.0, opacity=0.5)
+
+        pl.show(cpos="xy")
+
+    if render:
+        plot_fig()
+
+    #     stokes.constitutive_model = uw3.systems.constitutive_models.ViscousFlowModel(mesh.dim)
+    #     stokes.constitutive_model.material_properties = stokes.constitutive_model.Parameters(viscosity = viscosity )
+
+    #     # stokes.viscosity = viscosity
+
+    #     stokes.bodyforce = -1. * density * mesh.N.j
+
+    step = 0
+    time = 0.0
+
+    tSinker = np.zeros(nsteps)
+    ySinker = np.zeros(nsteps)
+
+    #### initial linear solve
+    stokes.viscosity = 1.0
+    stokes.constitutive_model.material_properties = (
+        stokes.constitutive_model.Parameters(viscosity=1.0)
+    )
+    # stokes.bodyforce =  -1* 1e-32 * mesh.N.j
+    stokes.bodyforce = -1 * density * mesh.N.j
+    # stokes.petsc_options["snes_type"]  = "ksponly"
+    stokes.solve()
+
+    ### add in NL viscosity
+    stokes.viscosity = viscosity
+    stokes.constitutive_model.material_properties = (
+        stokes.constitutive_model.Parameters(viscosity=viscosity)
+    )
+    stokes.bodyforce = -1 * density * mesh.N.j
+
+    while step < nsteps:
+
+        stokes.solve()
+
+        if uw3.mpi.rank == 0:
+            ymin = tracer[:, 1].min()
+            ySinker[step] = ymin
+            tSinker[step] = time
+            print(
+                f"Step: {str(step).rjust(3)}, time: {time:6.2f}, tracer:  {ymin:6.2f}"
+            )  # , vrms {vrms_val:.3e}")
+
+        ### estimate dt
+        dt = stokes.estimate_dt()
+
+        with swarm.access():
+            vel_on_particles = uw3.function.evaluate(
+                stokes.u.fn, swarm.particle_coordinates.data
+            )
+
+        ### advect swarm
+        with swarm.access(swarm.particle_coordinates):
+            swarm.particle_coordinates.data[:] += dt * vel_on_particles
+
+        vel_on_tracer = uw3.function.evaluate(stokes.u.fn, tracer)
+        tracer += dt * vel_on_tracer
+
+        # if MPI.COMM_WORLD.rank==0:
+        # print('step = {0:6d}; time = {1:.3e}; v_rms = {2:.3e}; height = {3:.3e}'
+        #       .format(step,time,vrms,ymin))
+
+        step += 1
+        time += dt
+
+    if uw3.mpi.rank == 0:
+        print(
+            "Initial position: t = {0:.3f}, y = {1:.3f}".format(tSinker[0], ySinker[0])
+        )
+        print(
+            "Final position:   t = {0:.3f}, y = {1:.3f}".format(
+                tSinker[nsteps - 1], ySinker[nsteps - 1]
+            )
+        )
+
+        uw2.utils.matplotlib_inline()
+        import matplotlib.pyplot as pyplot
+
+        fig = pyplot.figure()
+        fig.set_size_inches(12, 6)
+        ax = fig.add_subplot(1, 1, 1)
+        ax.plot(tSinker, ySinker)
+        ax.set_xlabel("Time")
+        ax.set_ylabel("Sinker position")
+
+        # fig1.show()
+
+        if render:
+            plot_fig()
+
+        return tSinker, ySinker
+
+
+
+
+
+
+
# %%
+tSinker_UW2, ySinker0_UW2, ySinker1_UW2 = uw2_stokesSinker()
+
+
+
+
+
+
+
# %%
+tSinker_UW3, ySinker_UW3 = uw3_stokesSinker()
+
+
+
+
+
+
+
# %%
+UW2_vel = (ySinker0_UW2[0] - ySinker0_UW2[-1]) / (tSinker_UW2[0] - tSinker_UW2[-1])
+UW3_vel = (ySinker_UW3[0] - ySinker_UW3[-1]) / (tSinker_UW3[0] - tSinker_UW3[-1])
+
+
+
+
+
+
+
# %%
+print(f"\n\n\n UW2 velocity: {UW2_vel}, UW3 velocity: {UW3_vel/2.} \n\n\n")
+
+
+
+
+
+
+
# %%
+if uw3.mpi.rank == 0:
+    import matplotlib.pyplot as pyplot
+
+    fig = pyplot.figure()
+    fig.set_size_inches(12, 6)
+    ax = fig.add_subplot(1, 1, 1)
+    ax.plot(tSinker_UW2, ySinker0_UW2, c="blue", ls="--", label="UW2")
+
+    ax.plot(tSinker_UW2, ySinker1_UW2, c="orange", ls="-.", label="UW2")
+
+    ax.plot(tSinker_UW3, ySinker_UW3, c="red", ls=":", label="UW3")
+
+    ax.legend()
+
+    ax.set_xlabel("Time")
+    ax.set_ylabel("Sinker position")
+
+    # fig1.show()
+
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-UW-2to3/output/README.html b/main/Notebooks/Examples-UW-2to3/output/README.html new file mode 100644 index 0000000..1a71c30 --- /dev/null +++ b/main/Notebooks/Examples-UW-2to3/output/README.html @@ -0,0 +1,519 @@ + + + + + + + + + + + UW2 -> UW3 model outputs — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

UW2 -> UW3 model outputs

+ +
+
+ +
+
+
+ + + + +
+ +
+

UW2 -> UW3 model outputs#

+

These files are NOT under version control

+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Utilities/Ex_Anisotropy.html b/main/Notebooks/Examples-Utilities/Ex_Anisotropy.html new file mode 100644 index 0000000..a43bc13 --- /dev/null +++ b/main/Notebooks/Examples-Utilities/Ex_Anisotropy.html @@ -0,0 +1,1034 @@ + + + + + + + + + + + Anisotropic Constitutive relationships in Underworld (pt 1) — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + +
+ +
+

Anisotropic Constitutive relationships in Underworld (pt 1)#

+

Introduction to how the anisotropic constitutive relationships in Underworld are formulated.

+

This may be helpful:

+

+

(Image from: https://serc.carleton.edu/NAGTWorkshops/mineralogy/mineral_physics/tensors.html) +This will also be helpful: https://en.wikipedia.org/wiki/Elasticity_tensor

+
+

Incompressibility#

+

If we apply constraints to the deformation, we expect to reduce the number of independent material constants. Incompressibility should reduce the number of independent material constants. In an isotropic medium (or a medium with cubic symmetry), incompressibility eliminates the one volumetric material modulus (e.g. the bulk modulus). In general anisotropic media, it is not the case that changes in pressure result in uniform expansion or contraction, and an incompressibility constraint reduces the number of independent material constants. In the transversely isotropic case, there are five independent materia constants in general, reducing to four when the material is incompressible.

+

It is not a given that the stiffness matrix is trivial to construct / meaningful for incompressible anisotropy and there is some discussion here: https://rastgaragah.wordpress.com/2013/03/12/incompressibility-of-linearly-elastic-material/ (identifies the issue but no resolution) and this explains in more detail: https://doi.org/10.1016/S0022-5096(01)00121-1.

+
+
+

Introduction to constitutive models#

+

Constitutive models relate fluxes of a quantity to the gradients of the unknowns. For example, in thermal diffusion, there is a relationship between heat flux and temperature gradients which is known as Fourier’s law and which involves an empirically determined thermal conductivity.

+
+\[ q_i = k \frac{ \partial T}{\partial x_i} \]
+

and in elasticity or fluid flow, we have a relationship between stresses and strain (gradients of the displacements) or strain-rate (gradients of velocity) respectively

+
+\[ \tau_{ij} = \mu \left( \frac{\partial u_i}{\partial x_j} + \frac{\partial u_j}{\partial x_i} \right) \]
+

where \(\eta\), here, is a material property (elastic shear modulus, viscosity) that we need to determine experimentally.

+

These material properties can be non-linear (they depend upon the quantities that they relate), and they can also be anisotropic (they vary with the orientation of the material). In the latter case, the material property is described through a consitutive tensor. For example:

+
+\[ q_i = k_{ij} \frac{ \partial T}{\partial x_j} \]
+

or

+
+\[ \tau_{ij} = \mu_{ijkl} \left( \frac{\partial u_k}{\partial x_l} + \frac{\partial u_l}{\partial x_k} \right) \]
+
+
+

Constitutive tensors#

+

In Underworld, the underlying implementation of any consitutive relationship is through the appropriate tensor representation, allowing for a very general formulation for most problems. The constitutive tensors are described symbolically using sympy expressions. For scalar equations, the rank 2 constitutive tensor (\(k_{ij}\)) can be expressed as a matrix without any loss of generality.

+

For vector equations, the constitutive tensor is of rank 4, but it is common in the engineering / finite element literature to transform the rank 4 tensor to a matrix representation. This has some benefits in terms of legibility and, perhaps, results in common code patterns across the formulation for scalar equations.

+

We can write the stress and strain rate tensors (\(\tau_{ij}\) and \(\dot\epsilon_{ij}\) respectively) as

+
+\[ \tau_I = \left( \tau_{00}, \tau_{11}, \tau_{22}, \tau_{12}, \tau_{02}, \tau_{01} \right) \]
+
+\[ \dot\varepsilon_I = \left( \dot\varepsilon_{00}, \dot\varepsilon_{11}, \dot\varepsilon_{22}, \dot\varepsilon_{12}, \dot\varepsilon_{02}, \dot\varepsilon_{01} \right) \]
+

where \(I\) indexes \(\{i,j\}\) in a specified order and symmetry of the tensor implies only six (in 3D) unique entries in the nine entries of a general tensor. With these definitions, the constitutive tensor can be written as \(C_{IJ}\) where \(I\), and \(J\) range over the independent degrees of freedom in stress and strain (rate). This is a re-writing of the full tensor, with the necessary symmetry required to respect the symmetry of \(\tau\) and \(\dot\varepsilon\)

+

The simplified notation comes at a cost. For example, \(\tau_I \dot\varepsilon_I\) is not equivalent to the tensor inner produce \(\tau_{ij} \dot\varepsilon_{ij}\), and \(C_{IJ}\) does not transform correctly for a rotation of coordinates.

+

However, a modest transformation of the matrix equations is helpful:

+
+\[ \tau^*_{I} = C^*_{IJ} \varepsilon^*_{J} \]
+

Where

+
+\[\tau^*_{I} = P_{IJ} \tau^*_J\]
+
+\[\varepsilon^*_I = P_{IJ} \varepsilon^*_J\]
+
+\[C^*_{IJ} = P_{IK} C_{KL} P_{LJ}\]
+

\(\mathbf{P}\) is the scaling matrix

+
+\[\begin{split}P = \left[\begin{matrix}1 & 0 & 0 & 0 & 0 & 0\\0 & 1 & 0 & 0 & 0 & 0\\0 & 0 & 1 & 0 & 0 & 0\\0 & 0 & 0 & \sqrt{2} & 0 & 0\\0 & 0 & 0 & 0 & \sqrt{2} & 0\\0 & 0 & 0 & 0 & 0 & \sqrt{2}\end{matrix}\right]\end{split}\]
+

In this form, known as the Mandel notation, \(\tau^*_I\varepsilon^*_I \equiv \tau_{ij} \varepsilon_{ij}\), and the fourth order identity matrix:

+
+\[I_{ijkl} = \frac{1}{2} \left( \delta_{ij}\delta_{kj} + \delta_{kl}\delta_{il} \right)\]
+

transforms to

+
+\[I_{IJ} = \delta_{IJ}\]
+
+
+

Mandel form, sympy tensorial form, Voigt form#

+

The rank 4 tensor form of the constitutive equations, \(c_{ijkl}\), has the following representation in sympy:

+
+\[\begin{split}\left[\begin{matrix}\left[\begin{matrix}c_{0 0 0 0} & c_{0 0 0 1}\\c_{0 0 1 0} & c_{0 0 1 1}\end{matrix}\right] & \left[\begin{matrix}c_{0 1 0 0} & c_{0 1 0 1}\\c_{0 1 1 0} & c_{0 1 1 1}\end{matrix}\right]\\\left[\begin{matrix}c_{1 0 0 0} & c_{1 0 0 1}\\c_{1 0 1 0} & c_{1 0 1 1}\end{matrix}\right] & \left[\begin{matrix}c_{1 1 0 0} & c_{1 1 0 1}\\c_{1 1 1 0} & c_{1 1 1 1}\end{matrix}\right]\end{matrix}\right]\end{split}\]
+

and the inner product \(\tau_{ij} = c_{ijkl} \varepsilon_{kl} \) is written

+
tau = sympy.tensorcontraction(
+            sympy.tensorcontraction(
+                   sympy.tensorproduct(c, epsilon),(3,5)),(2,3))
+
+
+

However, the sympy.Matrix module allows a much simpler expression

+
tau_star = C_star * epsilon_star
+tau = P.inv() * tau_star
+
+
+

which we adopt in underworld for the display and manipulation of constitutive tensors.

+
+

Voigt form#

+

Computation of the stress tensor using \(\tau^*_I = C^*_{IJ}\varepsilon^*_J\) is equivalent to the following

+
+\[ \mathbf{P} \mathbf{\tau} = \mathbf{P} \mathbf{C} \mathbf{P} \cdot \mathbf{P} \mathbf{\varepsilon} \]
+

multiply by \(\mathbf{P}^{-1} \) and collecting terms:

+
+\[ \mathbf{\tau} = \mathbf{C} \mathbf{P}^2 \mathbf{\varepsilon} \]
+

This is the Voigt form of the constitutive equations and is generally what you will find in a finite element textbook. The Voigt form of the constitutive matrix is the rearrangement of the rank 4 constitutive tensor (with no scaling), the strain rate vector is usually combined with \(\mathbf{P}^2\), and the stress vector is raw. For example:

+
+\[\begin{split} \left[\begin{matrix}\tau_{0 0}\\\tau_{1 1}\\\tau_{0 1}\end{matrix}\right] = + \left[\begin{matrix}\eta & 0 & 0\\0 & \eta & 0\\0 & 0 & \frac{\eta}{2}\end{matrix}\right] \left[\begin{matrix}\dot\varepsilon_{0 0}\\\dot\varepsilon_{1 1}\\2 \dot\varepsilon_{0 1}\end{matrix}\right] +\end{split}\]
+

A full discussion of this can be found in Brannon (2018) and Helnwein (2001).

+
+
+

References.#

+

Brannon, R. (2018). Rotation Reflection and Frame Changes Orthogonal tensors in computational engineering mechanics. IOP Publishing. https://doi.org/10.1088/978-0-7503-1454-1

+

Helnwein, P. (2001). Some remarks on the compressed matrix representation of symmetric second-order and fourth-order tensors. Computer Methods in Applied Mechanics and Engineering, 190(22–23), 2753–2770. https://doi.org/10.1016/S0045-7825(00)00263-2

+
+
+
+

Rotations#

+

The expressions for our anisotropic tensors are provided in a Canonical reference frame which exploits the symmetry of the material to present a compact form of the constitutive tensors. In a general orientation, a rotation tensor \(\cal R\) is required to transform to / from this canonical frame. For isotropic materials, \(\cal R\) is the identity matrix. For cubic materials, a single angle describes the offset of the two frames, and for transversely isotropic materials, we need to define a rotation matrix that aligns the normal to the symmetry plane with the vertical axis in the canonical frame. These examples have fewer degrees of freedom than a full rotation matrix and this fact can be used to simplify the general (rotated) form of the constitutive tensors.

+

Define \({\cal R}_{ij}\) as the rotation matrix that maps \(x\) onto the \(x' \) coordiate system, i.e.

+

\(\displaystyle a_i' = {\cal R}_{ij}\,a_j,\), for vectors,

+

which also has this property:

+

\(\displaystyle {\cal R}_{ki}\,{\cal R}_{kj } = \delta_{ij}.\)

+

Second order tensors transform as follows:

+

\(\displaystyle a_{ij}' = {\cal R}_{ik}\,{\cal R}_{jl}\,a_{kl},\)

+

and for higher rank tensors, we just continue …

+

\(\displaystyle a_{ijk}' = {\cal R}_{il}\,{\cal R}_{jm}\,{\cal R}_{kn}\,a_{lmn}.\)

+

In underworld tensor rotation is provided for rank 2 and rank 4 tensors by uw.maths.tensor.tensor_rotation(R, tensor_expression)

+
+

Example#

+

First we define the rotation for the transversely isotropic case which depends on the normal vector to the symmetry plane.

+

A rotation matrix for a transversely isotropic medium is defined by specifying the normal of the symmetry plane (\(\hat{\mathbf{n}} = \{ n_0, n_1, n_2\} \)). +The other orientations are arbitrary, so we simply derive them from \(\hat{\mathbf{n}}\) - one vector specified to be perpendicular in the horizontal plane and the third vector is then found from their cross product (\(\hat{\mathbf{s}}\) and \(\hat{\mathbf{t}}\) respectively)

+
+\[\begin{split}\cal{R} = \left[\begin{matrix}\frac{n_{1}}{\sqrt{n_{0}^{2} + n_{1}^{2}}} & - \frac{n_{0} n_{2}}{\sqrt{n_{0}^{2} + n_{1}^{2}}} & n_{0}\\- \frac{n_{0}}{\sqrt{n_{0}^{2} + n_{1}^{2}}} & - \frac{n_{1} n_{2}}{\sqrt{n_{0}^{2} + n_{1}^{2}}} & n_{1}\\0 & \frac{n_{0}^{2}}{\sqrt{n_{0}^{2} + n_{1}^{2}}} + \frac{n_{1}^{2}}{\sqrt{n_{0}^{2} + n_{1}^{2}}} & n_{2}\end{matrix}\right]\end{split}\]
+
+
+
## uw / sympy implementation
+
+import underworld3 as uw
+import sympy
+
+n = sympy.Matrix(sympy.symarray("n",(3,)))
+
+# Or give a specific value (just specify a vector, then normalise)
+# n = sympy.Matrix([1,1,1]) 
+# n /= sympy.sqrt(n.dot(n))
+
+if n[0] == 0:
+    s = sympy.Matrix([1,0,0])
+    t = sympy.Matrix([0,1,0])
+    R = sympy.eye(3)
+
+else:
+    s = sympy.Matrix((n[1] , -n[0], 0 ))
+    s /= sympy.sqrt(s.dot(s))
+    t = -n.cross(s) # complete the coordinate triad
+    R = sympy.BlockMatrix((s,t,n)).as_explicit()
+
+display(R)
+
+# print(sympy.latex(R, mode='plain'))
+
+
+
+
+
+
+
+

Validation#

+

A simple check on this is to rotate the isotropic constitutive tensor and validate that +it is invariant under rotation.

+
+\[\begin{split}C_{IJ} = \left[\begin{matrix}2 \eta_{0} & 0 & 0 & 0 & 0 & 0\\0 & 2 \eta_{0} & 0 & 0 & 0 & 0\\0 & 0 & 2 \eta_{0} & 0 & 0 & 0\\0 & 0 & 0 & 2 \eta_{0} & 0 & 0\\0 & 0 & 0 & 0 & 2 \eta_{0} & 0\\0 & 0 & 0 & 0 & 0 & 2 \eta_{0}\end{matrix}\right]\end{split}\]
+

Noting that the rotation of the Mandel or Voigt constitutive matrices is complicated by the \(\mathbf{P}\) scaling matrices, we compute rotations on the rank 4 tensor, \(c_{ijkl}\) and transform to the matrix forms as required. We denote the rotation from \(\{IJ\}\) coordinates to \(\{I'J'\}\) as

+
+\[C_{I'J'} = {\cal R}[C_{IJ}]\]
+

The rotated constitutive model has the following form:

+
+\[\begin{split}C_{I'J'} = \left[\begin{matrix}\frac{2 \eta_{0} \left(n_{0}^{2} n_{2}^{2} + n_{0}^{2} \left(n_{0}^{2} + n_{1}^{2}\right) + n_{1}^{2}\right)^{2}}{\left(n_{0}^{2} + n_{1}^{2}\right)^{2}} & \frac{2 \eta_{0} n_{0}^{2} n_{1}^{2} \left(n_{0}^{2} + n_{1}^{2} + n_{2}^{2} - 1\right)^{2}}{\left(n_{0}^{2} + n_{1}^{2}\right)^{2}} & 0 & 0 & 0 & \frac{2 \sqrt{2} \eta_{0} n_{0} n_{1} \left(n_{0}^{6} + 2 n_{0}^{4} n_{1}^{2} + 2 n_{0}^{4} n_{2}^{2} - n_{0}^{4} + n_{0}^{2} n_{1}^{4} + 2 n_{0}^{2} n_{1}^{2} n_{2}^{2} + n_{0}^{2} n_{2}^{4} - n_{0}^{2} n_{2}^{2} + n_{1}^{4} + n_{1}^{2} n_{2}^{2} - n_{1}^{2}\right)}{n_{0}^{4} + 2 n_{0}^{2} n_{1}^{2} + n_{1}^{4}}\\\frac{2 \eta_{0} n_{0}^{2} n_{1}^{2} \left(n_{0}^{2} + n_{1}^{2} + n_{2}^{2} - 1\right)^{2}}{\left(n_{0}^{2} + n_{1}^{2}\right)^{2}} & \frac{2 \eta_{0} \left(n_{0}^{2} + n_{1}^{2} n_{2}^{2} + n_{1}^{2} \left(n_{0}^{2} + n_{1}^{2}\right)\right)^{2}}{\left(n_{0}^{2} + n_{1}^{2}\right)^{2}} & 0 & 0 & 0 & \frac{2 \sqrt{2} \eta_{0} n_{0} n_{1} \left(n_{0}^{4} n_{1}^{2} + n_{0}^{4} + 2 n_{0}^{2} n_{1}^{4} + 2 n_{0}^{2} n_{1}^{2} n_{2}^{2} + n_{0}^{2} n_{2}^{2} - n_{0}^{2} + n_{1}^{6} + 2 n_{1}^{4} n_{2}^{2} - n_{1}^{4} + n_{1}^{2} n_{2}^{4} - n_{1}^{2} n_{2}^{2}\right)}{n_{0}^{4} + 2 n_{0}^{2} n_{1}^{2} + n_{1}^{4}}\\0 & 0 & 2 \eta_{0} \left(n_{0}^{2} + n_{1}^{2} + n_{2}^{2}\right)^{2} & 0 & 0 & 0\\0 & 0 & 0 & \frac{2 \eta_{0} \left(n_{0}^{2} + n_{1}^{2} + n_{2}^{2}\right) \left(n_{0}^{2} + n_{1}^{2} n_{2}^{2} + n_{1}^{2} \left(n_{0}^{2} + n_{1}^{2}\right)\right)}{n_{0}^{2} + n_{1}^{2}} & \frac{2 \eta_{0} n_{0} n_{1} \left(n_{0}^{2} + n_{1}^{2} + n_{2}^{2}\right) \left(n_{0}^{2} + n_{1}^{2} + n_{2}^{2} - 1\right)}{n_{0}^{2} + n_{1}^{2}} & 0\\0 & 0 & 0 & \frac{2 \eta_{0} n_{0} n_{1} \left(n_{0}^{2} + n_{1}^{2} + n_{2}^{2}\right) \left(n_{0}^{2} + n_{1}^{2} + n_{2}^{2} - 1\right)}{n_{0}^{2} + n_{1}^{2}} & \frac{2 \eta_{0} \left(n_{0}^{2} + n_{1}^{2} + n_{2}^{2}\right) \left(n_{0}^{2} n_{2}^{2} + n_{0}^{2} \left(n_{0}^{2} + n_{1}^{2}\right) + n_{1}^{2}\right)}{n_{0}^{2} + n_{1}^{2}} & 0\\\frac{2 \sqrt{2} \eta_{0} n_{0} n_{1} \left(n_{0}^{2} n_{2}^{2} + n_{0}^{2} \left(n_{0}^{2} + n_{1}^{2}\right) + n_{1}^{2}\right) \left(n_{0}^{2} + n_{1}^{2} + n_{2}^{2} - 1\right)}{\left(n_{0}^{2} + n_{1}^{2}\right)^{2}} & \frac{2 \sqrt{2} \eta_{0} n_{0} n_{1} \left(n_{0}^{2} + n_{1}^{2} n_{2}^{2} + n_{1}^{2} \left(n_{0}^{2} + n_{1}^{2}\right)\right) \left(n_{0}^{2} + n_{1}^{2} + n_{2}^{2} - 1\right)}{\left(n_{0}^{2} + n_{1}^{2}\right)^{2}} & 0 & 0 & 0 & \frac{2 \eta_{0} \left(n_{0}^{2} n_{2}^{2} \left(n_{0}^{2} + 2 n_{1}^{2} n_{2}^{2} + 2 n_{1}^{2} \left(n_{0}^{2} + n_{1}^{2}\right) - n_{1}^{2}\right) + n_{0}^{2} \left(n_{0}^{2} + n_{1}^{2}\right) \left(n_{0}^{2} + 2 n_{1}^{2} n_{2}^{2} + 2 n_{1}^{2} \left(n_{0}^{2} + n_{1}^{2}\right) - n_{1}^{2}\right) + n_{1}^{2} \cdot \left(2 n_{0}^{2} + n_{2}^{2} \left(- n_{0}^{2} + n_{1}^{2}\right) - \left(n_{0}^{2} - n_{1}^{2}\right) \left(n_{0}^{2} + n_{1}^{2}\right)\right)\right)}{\left(n_{0}^{2} + n_{1}^{2}\right)^{2}}\end{matrix}\right]\end{split}\]
+

However, \(\{n_0, n_1, n_2\}\) are not independent because \(\hat{\mathbf{n}}\) is a unit vector. If we add this information and simplify, we recover the isotropic form of \(C\)

+
+
+
# construct a symbolic, isotropic matrix (Mandel form)
+
+eta0 = sympy.symbols("\eta_0")
+C_IJm = 2 * sympy.Matrix.diag([eta0]*6)
+display(C_IJm)
+
+## Rotate the matrix 
+
+c_ijkl = uw.maths.tensor.mandel_to_rank4(C_IJm, 3)
+C_IJv = uw.maths.tensor.rank4_to_voigt(c_ijkl, 3)
+c_ijkl_R = sympy.simplify(uw.maths.tensor.tensor_rotation(R, c_ijkl))
+C_IJm_R = sympy.simplify(uw.maths.tensor.rank4_to_mandel(c_ijkl_R, 3))
+display(C_IJm_R)
+
+## Is this really invariant under rotation ?? 
+## Have to do some manipulation to identify the unit-vector component relationships
+
+C_IJm_R_s1 = C_IJm_R.subs(n[0]**2+n[1]**2+n[2]**2,1)
+C_IJm_R_s2 = C_IJm_R_s1.subs(n[0]**2+n[1]**2, 1-n[2]**2).applyfunc(sympy.factor)
+C_IJm_R_s2.subs(n[0]**2+n[1]**2, 1-n[2]**2).simplify()
+
+
+
+
+
+
+

Muhlhaus / Moresi transversely isotropic tensor#

+

The Muhlhaus / Moresi transversely isotropic consitituve model is designed to model +materials that have a single (usually) weak plane (e.g. an embedded fault).

+

The constitutive model is

+
+\[\begin{split}\left[\begin{matrix}2 \eta_{0} & 0 & 0 & 0 & 0 & 0\\0 & 2 \eta_{0} & 0 & 0 & 0 & 0\\0 & 0 & 2 \eta_{0} & 0 & 0 & 0\\0 & 0 & 0 & - 2 \Delta\eta + 2 \eta_{0} & 0 & 0\\0 & 0 & 0 & 0 & - 2 \Delta\eta + 2 \eta_{0} & 0\\0 & 0 & 0 & 0 & 0 & 2 \eta_{0}\end{matrix}\right]\end{split}\]
+

where \(\Delta\eta\) represents the change (usually reduction) in viscosity.

+

Rotation using \(\cal R\) as defined from the normal to the weak plane (above) gives (Mandel form):

+
+\[\begin{split}\left[\begin{matrix}- \frac{4 \Delta\eta n_{0}^{2} \left(n_{0}^{2} n_{2}^{2} + n_{1}^{2}\right)}{n_{0}^{2} + n_{1}^{2}} + 2 \eta_{0} & \frac{4 \Delta\eta n_{0}^{2} n_{1}^{2} \cdot \left(1 - n_{2}^{2}\right)}{n_{0}^{2} + n_{1}^{2}} & 4 \Delta\eta n_{0}^{2} n_{2}^{2} & \frac{2 \sqrt{2} \Delta\eta n_{0}^{2} n_{1} n_{2} \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2} + 1\right)}{n_{0}^{2} + n_{1}^{2}} & \frac{2 \sqrt{2} \Delta\eta n_{0} n_{2} \left(n_{0}^{2} \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2}\right) - n_{1}^{2}\right)}{n_{0}^{2} + n_{1}^{2}} & \frac{2 \sqrt{2} \Delta\eta n_{0} n_{1} \left(- 2 n_{0}^{2} n_{2}^{2} + n_{0}^{2} - n_{1}^{2}\right)}{n_{0}^{2} + n_{1}^{2}}\\\frac{4 \Delta\eta n_{0}^{2} n_{1}^{2} \cdot \left(1 - n_{2}^{2}\right)}{n_{0}^{2} + n_{1}^{2}} & - \frac{4 \Delta\eta n_{1}^{2} \left(n_{0}^{2} + n_{1}^{2} n_{2}^{2}\right)}{n_{0}^{2} + n_{1}^{2}} + 2 \eta_{0} & 4 \Delta\eta n_{1}^{2} n_{2}^{2} & - \frac{2 \sqrt{2} \Delta\eta n_{1} n_{2} \left(n_{0}^{2} - n_{1}^{2} \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2}\right)\right)}{n_{0}^{2} + n_{1}^{2}} & \frac{2 \sqrt{2} \Delta\eta n_{0} n_{1}^{2} n_{2} \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2} + 1\right)}{n_{0}^{2} + n_{1}^{2}} & \frac{2 \sqrt{2} \Delta\eta n_{0} n_{1} \left(- n_{0}^{2} - 2 n_{1}^{2} n_{2}^{2} + n_{1}^{2}\right)}{n_{0}^{2} + n_{1}^{2}}\\4 \Delta\eta n_{0}^{2} n_{2}^{2} & 4 \Delta\eta n_{1}^{2} n_{2}^{2} & - 4 \Delta\eta n_{2}^{2} \left(n_{0}^{2} + n_{1}^{2}\right) + 2 \eta_{0} & 2 \sqrt{2} \Delta\eta n_{1} n_{2} \left(- n_{0}^{2} - n_{1}^{2} + n_{2}^{2}\right) & 2 \sqrt{2} \Delta\eta n_{0} n_{2} \left(- n_{0}^{2} - n_{1}^{2} + n_{2}^{2}\right) & 4 \sqrt{2} \Delta\eta n_{0} n_{1} n_{2}^{2}\\\frac{2 \sqrt{2} \Delta\eta n_{0}^{2} n_{1} n_{2} \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2} + 1\right)}{n_{0}^{2} + n_{1}^{2}} & \frac{2 \sqrt{2} \Delta\eta n_{1} n_{2} \left(n_{0}^{2} n_{1}^{2} - n_{0}^{2} + n_{1}^{4} - n_{1}^{2} n_{2}^{2}\right)}{n_{0}^{2} + n_{1}^{2}} & 2 \sqrt{2} \Delta\eta n_{1} n_{2} \left(- n_{0}^{2} - n_{1}^{2} + n_{2}^{2}\right) & - \frac{2 \Delta\eta \left(n_{0}^{2} n_{2}^{2} - n_{1}^{2} n_{2}^{2} \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2}\right) + n_{1}^{2} \left(n_{0}^{2} + n_{1}^{2}\right) \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2}\right)\right)}{n_{0}^{2} + n_{1}^{2}} + 2 \eta_{0} & \frac{2 \Delta\eta n_{0} n_{1} \left(n_{2}^{2} \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2}\right) + n_{2}^{2} - \left(n_{0}^{2} + n_{1}^{2}\right) \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2}\right)\right)}{n_{0}^{2} + n_{1}^{2}} & \frac{2 \Delta\eta n_{0} n_{2} \cdot \left(2 n_{0}^{2} n_{1}^{2} - n_{0}^{2} + 2 n_{1}^{4} - 2 n_{1}^{2} n_{2}^{2} + n_{1}^{2}\right)}{n_{0}^{2} + n_{1}^{2}}\\\frac{2 \sqrt{2} \Delta\eta n_{0} n_{2} \left(n_{0}^{4} + n_{0}^{2} n_{1}^{2} - n_{0}^{2} n_{2}^{2} - n_{1}^{2}\right)}{n_{0}^{2} + n_{1}^{2}} & \frac{2 \sqrt{2} \Delta\eta n_{0} n_{1}^{2} n_{2} \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2} + 1\right)}{n_{0}^{2} + n_{1}^{2}} & 2 \sqrt{2} \Delta\eta n_{0} n_{2} \left(- n_{0}^{2} - n_{1}^{2} + n_{2}^{2}\right) & \frac{2 \Delta\eta n_{0} n_{1} \left(n_{2}^{2} \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2}\right) + n_{2}^{2} - \left(n_{0}^{2} + n_{1}^{2}\right) \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2}\right)\right)}{n_{0}^{2} + n_{1}^{2}} & - \frac{2 \Delta\eta \left(- n_{0}^{2} n_{2}^{2} \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2}\right) + n_{0}^{2} \left(n_{0}^{2} + n_{1}^{2}\right) \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2}\right) + n_{1}^{2} n_{2}^{2}\right)}{n_{0}^{2} + n_{1}^{2}} + 2 \eta_{0} & \frac{2 \Delta\eta n_{1} n_{2} \cdot \left(2 n_{0}^{4} + 2 n_{0}^{2} n_{1}^{2} - 2 n_{0}^{2} n_{2}^{2} + n_{0}^{2} - n_{1}^{2}\right)}{n_{0}^{2} + n_{1}^{2}}\\\frac{2 \sqrt{2} \Delta\eta n_{0} n_{1} \left(- 2 n_{0}^{2} n_{2}^{2} + n_{0}^{2} - n_{1}^{2}\right)}{n_{0}^{2} + n_{1}^{2}} & \frac{2 \sqrt{2} \Delta\eta n_{0} n_{1} \left(- n_{0}^{2} - 2 n_{1}^{2} n_{2}^{2} + n_{1}^{2}\right)}{n_{0}^{2} + n_{1}^{2}} & 4 \sqrt{2} \Delta\eta n_{0} n_{1} n_{2}^{2} & \frac{2 \Delta\eta n_{0} n_{2} \left(- n_{0}^{2} + 2 n_{1}^{2} \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2}\right) + n_{1}^{2}\right)}{n_{0}^{2} + n_{1}^{2}} & \frac{2 \Delta\eta n_{1} n_{2} \cdot \left(2 n_{0}^{4} + 2 n_{0}^{2} n_{1}^{2} - 2 n_{0}^{2} n_{2}^{2} + n_{0}^{2} - n_{1}^{2}\right)}{n_{0}^{2} + n_{1}^{2}} & \frac{2 \Delta\eta \left(- n_{0}^{4} - 4 n_{0}^{2} n_{1}^{2} n_{2}^{2} + 2 n_{0}^{2} n_{1}^{2} - n_{1}^{4}\right)}{n_{0}^{2} + n_{1}^{2}} + 2 \eta_{0}\end{matrix}\right]\end{split}\]
+

and we can easily demonstrate that this collapses back to the isotropic form if \(\Delta\eta \leftarrow 0\). We can also show this is equivalent to the alternate expression for the tensor provided in the original papers (an exercise for the reader !!).

+

The underworld implementation is:

+
+
+
# Muhlhaus definition of C_IJ (mandel form)
+
+eta1 = sympy.symbols("\eta_1")
+delta_eta = sympy.symbols("\Delta\eta")
+
+C_ijkl_MM = uw.maths.tensor.rank4_identity(3) * 0
+C_IJm_MM = uw.maths.tensor.rank4_to_mandel(C_ijkl_MM, 3)
+C_IJm_MM[0,0] = 2*eta0
+C_IJm_MM[1,1] = 2*eta0
+C_IJm_MM[2,2] = 2*eta0
+C_IJm_MM[3,3] = 2*(eta0-delta_eta)  # yz
+C_IJm_MM[4,4] = 2*(eta0-delta_eta)  # xz
+C_IJm_MM[5,5] = 2*eta0  # xy
+
+display(C_IJm_MM)
+
+## We know that the isotropic part is invariant under rotation, so we only need to 
+## examine the non-isotropic part.
+
+C_ijkl_MM = uw.maths.tensor.mandel_to_rank4(C_IJm_MM - C_IJm , 3)
+C_ijkl_MM_R = sympy.simplify(uw.maths.tensor.tensor_rotation(R, C_ijkl_MM))
+
+C_IJm_MM_R = sympy.simplify(uw.maths.tensor.rank4_to_mandel(C_ijkl_MM_R, 3)) + C_IJm
+C_IJv_MM_R = sympy.simplify(uw.maths.tensor.rank4_to_voigt(C_ijkl_MM_R, 3)) + C_IJv
+
+display(C_IJm_MM_R)
+
+# Check what happens if we set delta eta to zero
+C_IJm_MM_iso = C_IJm_MM_R.subs(delta_eta,0).applyfunc(sympy.factor).subs(n[0]**2+n[1]**2, 1-n[2]**2).simplify()
+display(C_IJm_MM_iso)
+
+
+
+
+
+
+

Han & Wahr (full transverse isotropic tensor)#

+

In the Han & Wahr paper, the expression for incompressible transverse-isotropy is as follows

+
+\[\begin{split}\left[\begin{matrix}2 \eta_{0} + \mu_{0} & \mu_{0} & 0 & 0 & 0 & 0\\\mu_{0} & 2 \eta_{0} + \mu_{0} & 0 & 0 & 0 & 0\\0 & 0 & - 2 \Delta\eta + 2 \eta_{0} + \mu_{1} & 0 & 0 & 0\\0 & 0 & 0 & - 2 \Delta\eta + 2 \eta_{0} & 0 & 0\\0 & 0 & 0 & 0 & - 2 \Delta\eta + 2 \eta_{0} & 0\\0 & 0 & 0 & 0 & 0 & 2 \eta_{0}\end{matrix}\right]\end{split}\]
+

Note that the notation differs from their paper. I have replaced their \(\nu1, \nu2\) with \(\eta0, \eta1\) to be consistent with the forms defined above. I have replaced their \(\eta\) with \(\mu\) to avoid the confusion that results from the first change.

+

Applying the rotation, \(\cal R\) and attempting to coerce sympy to simplify the constitutive matrix:

+
+\[\begin{split}\left[\begin{matrix}\frac{2 \Delta\eta n_{0}^{4} n_{2}^{4} - 2 \Delta\eta n_{0}^{4} + 4 \Delta\eta n_{0}^{2} n_{1}^{2} n_{2}^{2} - 4 \Delta\eta n_{0}^{2} n_{1}^{2} + 2 \eta_{0} n_{2}^{4} - 4 \eta_{0} n_{2}^{2} + 2 \eta_{0} + \mu_{0} n_{0}^{4} n_{2}^{4} + 2 \mu_{0} n_{0}^{2} n_{1}^{2} n_{2}^{2} + \mu_{0} n_{1}^{4} + \mu_{1} n_{0}^{4} n_{2}^{4} - 2 \mu_{1} n_{0}^{4} n_{2}^{2} + \mu_{1} n_{0}^{4}}{\left(n_{2} - 1\right)^{2} \left(n_{2} + 1\right)^{2}} & \frac{2 \Delta\eta n_{0}^{2} n_{1}^{2} n_{2}^{4} - 4 \Delta\eta n_{0}^{2} n_{1}^{2} n_{2}^{2} + 2 \Delta\eta n_{0}^{2} n_{1}^{2} + \mu_{0} n_{0}^{4} n_{2}^{2} + \mu_{0} n_{0}^{2} n_{1}^{2} n_{2}^{4} + \mu_{0} n_{0}^{2} n_{1}^{2} + \mu_{0} n_{1}^{4} n_{2}^{2} + \mu_{1} n_{0}^{2} n_{1}^{2} n_{2}^{4} - 2 \mu_{1} n_{0}^{2} n_{1}^{2} n_{2}^{2} + \mu_{1} n_{0}^{2} n_{1}^{2}}{\left(n_{2} - 1\right)^{2} \left(n_{2} + 1\right)^{2}} & 2 \Delta\eta n_{0}^{2} n_{2}^{2} + \mu_{0} n_{0}^{2} n_{2}^{2} + \mu_{0} n_{1}^{2} + \mu_{1} n_{0}^{2} n_{2}^{2} & \frac{\sqrt{2} n_{1} n_{2} \cdot \left(2 \Delta\eta n_{0}^{2} n_{2}^{2} - 2 \Delta\eta n_{0}^{2} + \mu_{0} n_{0}^{2} n_{2}^{2} + \mu_{0} n_{1}^{2} - \mu_{1} n_{0}^{4} - \mu_{1} n_{0}^{2} n_{1}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & \frac{\sqrt{2} n_{0} n_{2} \cdot \left(2 \Delta\eta n_{0}^{2} n_{2}^{2} + 2 \Delta\eta n_{1}^{2} + \mu_{0} n_{0}^{2} n_{2}^{2} + \mu_{0} n_{1}^{2} - \mu_{1} n_{0}^{4} - \mu_{1} n_{0}^{2} n_{1}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & \frac{\sqrt{2} n_{0} n_{1} \cdot \left(2 \Delta\eta n_{0}^{2} n_{2}^{2} + 2 \Delta\eta n_{1}^{2} + \mu_{0} n_{0}^{2} n_{2}^{2} + \mu_{0} n_{1}^{2} + \mu_{1} n_{0}^{2} n_{2}^{2} - \mu_{1} n_{0}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)}\\\frac{2 \Delta\eta n_{0}^{2} n_{1}^{2} n_{2}^{4} - 4 \Delta\eta n_{0}^{2} n_{1}^{2} n_{2}^{2} + 2 \Delta\eta n_{0}^{2} n_{1}^{2} + \mu_{0} n_{0}^{4} n_{2}^{2} + \mu_{0} n_{0}^{2} n_{1}^{2} n_{2}^{4} + \mu_{0} n_{0}^{2} n_{1}^{2} + \mu_{0} n_{1}^{4} n_{2}^{2} + \mu_{1} n_{0}^{2} n_{1}^{2} n_{2}^{4} - 2 \mu_{1} n_{0}^{2} n_{1}^{2} n_{2}^{2} + \mu_{1} n_{0}^{2} n_{1}^{2}}{\left(n_{2} - 1\right)^{2} \left(n_{2} + 1\right)^{2}} & \frac{4 \Delta\eta n_{0}^{2} n_{1}^{2} n_{2}^{2} - 4 \Delta\eta n_{0}^{2} n_{1}^{2} + 2 \Delta\eta n_{1}^{4} n_{2}^{4} - 2 \Delta\eta n_{1}^{4} + 2 \eta_{0} n_{2}^{4} - 4 \eta_{0} n_{2}^{2} + 2 \eta_{0} + \mu_{0} n_{0}^{4} + 2 \mu_{0} n_{0}^{2} n_{1}^{2} n_{2}^{2} + \mu_{0} n_{1}^{4} n_{2}^{4} + \mu_{1} n_{1}^{4} n_{2}^{4} - 2 \mu_{1} n_{1}^{4} n_{2}^{2} + \mu_{1} n_{1}^{4}}{\left(n_{2} - 1\right)^{2} \left(n_{2} + 1\right)^{2}} & 2 \Delta\eta n_{1}^{2} n_{2}^{2} + \mu_{0} n_{0}^{2} + \mu_{0} n_{1}^{2} n_{2}^{2} + \mu_{1} n_{1}^{2} n_{2}^{2} & \frac{\sqrt{2} n_{1} n_{2} \cdot \left(2 \Delta\eta n_{0}^{2} + 2 \Delta\eta n_{1}^{2} n_{2}^{2} + \mu_{0} n_{0}^{2} + \mu_{0} n_{1}^{2} n_{2}^{2} - \mu_{1} n_{0}^{2} n_{1}^{2} - \mu_{1} n_{1}^{4}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & \frac{\sqrt{2} n_{0} n_{2} \cdot \left(2 \Delta\eta n_{1}^{2} n_{2}^{2} - 2 \Delta\eta n_{1}^{2} + \mu_{0} n_{0}^{2} + \mu_{0} n_{1}^{2} n_{2}^{2} - \mu_{1} n_{0}^{2} n_{1}^{2} - \mu_{1} n_{1}^{4}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & \frac{\sqrt{2} n_{0} n_{1} \cdot \left(2 \Delta\eta n_{0}^{2} + 2 \Delta\eta n_{1}^{2} n_{2}^{2} + \mu_{0} n_{0}^{2} + \mu_{0} n_{1}^{2} n_{2}^{2} + \mu_{1} n_{1}^{2} n_{2}^{2} - \mu_{1} n_{1}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)}\\2 \Delta\eta n_{0}^{2} n_{2}^{2} + \mu_{0} n_{0}^{2} n_{2}^{2} + \mu_{0} n_{1}^{2} + \mu_{1} n_{0}^{2} n_{2}^{2} & 2 \Delta\eta n_{1}^{2} n_{2}^{2} + \mu_{0} n_{0}^{2} + \mu_{0} n_{1}^{2} n_{2}^{2} + \mu_{1} n_{1}^{2} n_{2}^{2} & 2 \Delta\eta n_{2}^{4} - 4 \Delta\eta n_{2}^{2} + 2 \eta_{0} + \mu_{0} n_{2}^{4} - 2 \mu_{0} n_{2}^{2} + \mu_{0} + \mu_{1} n_{2}^{4} & - \sqrt{2} n_{1} n_{2} \cdot \left(2 \Delta\eta n_{0}^{2} + 2 \Delta\eta n_{1}^{2} + \mu_{0} n_{0}^{2} + \mu_{0} n_{1}^{2} - \mu_{1} n_{2}^{2}\right) & - \sqrt{2} n_{0} n_{2} \cdot \left(2 \Delta\eta n_{0}^{2} + 2 \Delta\eta n_{1}^{2} + \mu_{0} n_{0}^{2} + \mu_{0} n_{1}^{2} - \mu_{1} n_{2}^{2}\right) & \sqrt{2} n_{0} n_{1} \cdot \left(2 \Delta\eta n_{2}^{2} + \mu_{0} n_{2}^{2} - \mu_{0} + \mu_{1} n_{2}^{2}\right)\\\frac{\sqrt{2} n_{1} n_{2} \cdot \left(2 \Delta\eta n_{0}^{2} n_{2}^{2} - 2 \Delta\eta n_{0}^{2} + \mu_{0} n_{0}^{2} n_{2}^{2} + \mu_{0} n_{1}^{2} + \mu_{1} n_{0}^{2} n_{2}^{2} - \mu_{1} n_{0}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & \frac{\sqrt{2} n_{1} n_{2} \cdot \left(2 \Delta\eta n_{0}^{2} + 2 \Delta\eta n_{1}^{2} n_{2}^{2} + \mu_{0} n_{0}^{2} + \mu_{0} n_{1}^{2} n_{2}^{2} + \mu_{1} n_{1}^{2} n_{2}^{2} - \mu_{1} n_{1}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & - \sqrt{2} n_{1} n_{2} \cdot \left(2 \Delta\eta n_{0}^{2} + 2 \Delta\eta n_{1}^{2} + \mu_{0} n_{0}^{2} + \mu_{0} n_{1}^{2} - \mu_{1} n_{2}^{2}\right) & \frac{2 \left(\Delta\eta n_{0}^{2} n_{2}^{2} + 2 \Delta\eta n_{1}^{2} n_{2}^{4} - 2 \Delta\eta n_{1}^{2} n_{2}^{2} + \Delta\eta n_{1}^{2} + \eta_{0} n_{2}^{2} - \eta_{0} + \mu_{0} n_{1}^{2} n_{2}^{4} - \mu_{0} n_{1}^{2} n_{2}^{2} + \mu_{1} n_{1}^{2} n_{2}^{4} - \mu_{1} n_{1}^{2} n_{2}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & 2 n_{0} n_{1} \cdot \left(2 \Delta\eta n_{2}^{2} - \Delta\eta + \mu_{0} n_{2}^{2} + \mu_{1} n_{2}^{2}\right) & \frac{2 n_{0} n_{2} \left(\Delta\eta n_{0}^{2} + 2 \Delta\eta n_{1}^{2} n_{2}^{2} - \Delta\eta n_{1}^{2} + \mu_{0} n_{1}^{2} n_{2}^{2} - \mu_{0} n_{1}^{2} + \mu_{1} n_{1}^{2} n_{2}^{2} - \mu_{1} n_{1}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)}\\\frac{\sqrt{2} n_{0} n_{2} \cdot \left(2 \Delta\eta n_{0}^{2} n_{2}^{2} + 2 \Delta\eta n_{1}^{2} + \mu_{0} n_{0}^{2} n_{2}^{2} + \mu_{0} n_{1}^{2} + \mu_{1} n_{0}^{2} n_{2}^{2} - \mu_{1} n_{0}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & \frac{\sqrt{2} n_{0} n_{2} \cdot \left(2 \Delta\eta n_{1}^{2} n_{2}^{2} - 2 \Delta\eta n_{1}^{2} + \mu_{0} n_{0}^{2} + \mu_{0} n_{1}^{2} n_{2}^{2} + \mu_{1} n_{1}^{2} n_{2}^{2} - \mu_{1} n_{1}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & - \sqrt{2} n_{0} n_{2} \cdot \left(2 \Delta\eta n_{0}^{2} + 2 \Delta\eta n_{1}^{2} + \mu_{0} n_{0}^{2} + \mu_{0} n_{1}^{2} - \mu_{1} n_{2}^{2}\right) & 2 n_{0} n_{1} \cdot \left(2 \Delta\eta n_{2}^{2} - \Delta\eta + \mu_{0} n_{2}^{2} + \mu_{1} n_{2}^{2}\right) & \frac{2 \cdot \left(2 \Delta\eta n_{0}^{2} n_{2}^{4} - 2 \Delta\eta n_{0}^{2} n_{2}^{2} + \Delta\eta n_{0}^{2} + \Delta\eta n_{1}^{2} n_{2}^{2} + \eta_{0} n_{2}^{2} - \eta_{0} + \mu_{0} n_{0}^{2} n_{2}^{4} - \mu_{0} n_{0}^{2} n_{2}^{2} + \mu_{1} n_{0}^{2} n_{2}^{4} - \mu_{1} n_{0}^{2} n_{2}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & \frac{2 n_{1} n_{2} \cdot \left(2 \Delta\eta n_{0}^{2} n_{2}^{2} - \Delta\eta n_{0}^{2} + \Delta\eta n_{1}^{2} + \mu_{0} n_{0}^{2} n_{2}^{2} - \mu_{0} n_{0}^{2} + \mu_{1} n_{0}^{2} n_{2}^{2} - \mu_{1} n_{0}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)}\\\frac{\sqrt{2} n_{0} n_{1} \cdot \left(2 \Delta\eta n_{0}^{2} n_{2}^{2} + 2 \Delta\eta n_{1}^{2} + \mu_{0} n_{0}^{2} n_{2}^{2} + \mu_{0} n_{1}^{2} + \mu_{1} n_{0}^{2} n_{2}^{2} - \mu_{1} n_{0}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & \frac{\sqrt{2} n_{0} n_{1} \cdot \left(2 \Delta\eta n_{0}^{2} + 2 \Delta\eta n_{1}^{2} n_{2}^{2} + \mu_{0} n_{0}^{2} + \mu_{0} n_{1}^{2} n_{2}^{2} + \mu_{1} n_{1}^{2} n_{2}^{2} - \mu_{1} n_{1}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & \sqrt{2} n_{0} n_{1} \cdot \left(2 \Delta\eta n_{2}^{2} + \mu_{0} n_{2}^{2} - \mu_{0} + \mu_{1} n_{2}^{2}\right) & \frac{2 n_{0} n_{2} \left(\Delta\eta n_{0}^{2} + 2 \Delta\eta n_{1}^{2} n_{2}^{2} - \Delta\eta n_{1}^{2} + \mu_{0} n_{1}^{2} n_{2}^{2} - \mu_{0} n_{1}^{2} - \mu_{1} n_{0}^{2} n_{1}^{2} - \mu_{1} n_{1}^{4}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & \frac{2 n_{1} n_{2} \cdot \left(2 \Delta\eta n_{0}^{2} n_{2}^{2} - \Delta\eta n_{0}^{2} + \Delta\eta n_{1}^{2} + \mu_{0} n_{0}^{2} n_{2}^{2} - \mu_{0} n_{0}^{2} - \mu_{1} n_{0}^{4} - \mu_{1} n_{0}^{2} n_{1}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & \frac{2 \left(\Delta\eta n_{0}^{4} + 2 \Delta\eta n_{0}^{2} n_{1}^{2} n_{2}^{2} + \Delta\eta n_{1}^{4} + \eta_{0} n_{2}^{2} - \eta_{0} + \mu_{0} n_{0}^{2} n_{1}^{2} n_{2}^{2} - \mu_{0} n_{0}^{2} n_{1}^{2} + \mu_{1} n_{0}^{2} n_{1}^{2} n_{2}^{2} - \mu_{1} n_{0}^{2} n_{1}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)}\end{matrix}\right]\end{split}\]
+

The underworld / sympy implementation follows:

+
+
+
## The full incompressible, trans-iso model (4 independent unknowns) from Han and Wahr paper
+
+# Extra viscosity terms
+mu0 = sympy.symbols(r"\mu_0")
+mu1 = sympy.symbols(r"\mu_1")
+
+I = uw.maths.tensor.rank4_identity(3) * 0
+C_IJm_HW = uw.maths.tensor.rank4_to_mandel(I, 3)
+C_IJm_HW[0,0] = mu0 + 2 * eta0 
+C_IJm_HW[0,1] = mu0  
+C_IJm_HW[1,0] = mu0  
+C_IJm_HW[1,1] = mu0 + 2 * eta0
+C_IJm_HW[2,2] = mu1 + 2 * (eta0 - delta_eta)
+C_IJm_HW[3,3] = 2 * (eta0 - delta_eta) # yz
+C_IJm_HW[4,4] = 2 * (eta0 - delta_eta)  # xz
+C_IJm_HW[5,5] = 2 * eta0  # xy
+
+display(C_IJm_HW)
+
+
+
+
+
+
+

Rotation (symbolic)#

+

Compute the rotation (of the non-isotropic part)

+
+
+
C_ijkl_HW = uw.maths.tensor.mandel_to_rank4(C_IJm_HW - C_IJm, 3)
+display(uw.maths.tensor.rank4_to_mandel(C_ijkl_HW, 3)) 
+
+C_ijkl_HW_R = sympy.simplify(uw.maths.tensor.tensor_rotation(R, C_ijkl_HW))
+C_IJm_HW_R = sympy.simplify(uw.maths.tensor.rank4_to_mandel(C_ijkl_HW_R, 3)) + C_IJm
+
+display(C_IJm_HW_R)
+
+
+
+
+
+
+
## Maybe this can be simplified if we use the unit vector relationships among n0,n1,n2
+
+C_IJm_HW_R_s1 = C_IJm_HW_R.subs(n[0]**2+n[1]**2+n[2]**2,1)
+C_IJm_HW_R_s2 = C_IJm_HW_R_s1.subs(n[0]**2+n[1]**2, 1-n[2]**2).applyfunc(sympy.factor)
+display(C_IJm_HW_R_s2)
+
+## Yeah, maybe not ... 
+
+
+
+
+
+
+

Orthotropic medium#

+

Note all the caveats above regarding incompressibility. The Browaeys & Chevrot elastic tensors have a bulk modulus term, so it is not completely obvious how to square the assumptions in the first two implementations with this set.

+

The full formulation should look like this:

+
+\[\begin{split}\left[\begin{matrix}2 \eta_{00} & 2 \eta_{01} & 2 \eta_{02} & 0 & 0 & 0\\2 \eta_{01} & 2 \eta_{11} & 2 \eta_{12} & 0 & 0 & 0\\2 \eta_{02} & 2 \eta_{12} & 2 \eta_{22} & 0 & 0 & 0\\0 & 0 & 0 & 2 \eta_{33} & 0 & 0\\0 & 0 & 0 & 0 & 2 \eta_{44} & 0\\0 & 0 & 0 & 0 & 0 & 2 \eta_{55}\end{matrix}\right]\end{split}\]
+

Rotation in this case should be general as it is no longer enough to specify the symmetry plane.

+
+\[\begin{split}\left[\begin{matrix}s_{0} & t_{0} & n_{0}\\s_{1} & t_{1} & n_{1}\\s_{2} & t_{2} & n_{2}\end{matrix}\right]\end{split}\]
+

\(\hat{\mathbf{n}}\), \(\hat{\mathbf{s}}\) and \(\hat{\mathbf{t}}\) are an arbitrary orthogonal triad of unit vectors (we keep the notation from the Mühlhaus formulation). It is probably not useful to code up this form.

+
+\[\begin{split}\left[\begin{matrix}2 \eta_{00} s_{0}^{4} + 4 \eta_{01} s_{0}^{2} t_{0}^{2} + 4 \eta_{02} n_{0}^{2} s_{0}^{2} + 2 \eta_{11} t_{0}^{4} + 4 \eta_{12} n_{0}^{2} t_{0}^{2} + 2 \eta_{22} n_{0}^{4} + 4 \eta_{33} n_{0}^{2} t_{0}^{2} + 4 \eta_{44} n_{0}^{2} s_{0}^{2} + 4 \eta_{55} s_{0}^{2} t_{0}^{2} & 2 n_{0} \left(\eta_{33} n_{1} t_{0} t_{1} + \eta_{44} n_{1} s_{0} s_{1} + n_{0} \left(\eta_{02} s_{1}^{2} + \eta_{12} t_{1}^{2} + \eta_{22} n_{1}^{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{0} n_{1} s_{1} + \eta_{55} s_{1} t_{0} t_{1} + s_{0} \left(\eta_{00} s_{1}^{2} + \eta_{01} t_{1}^{2} + \eta_{02} n_{1}^{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{0} n_{1} t_{1} + \eta_{55} s_{0} s_{1} t_{1} + t_{0} \left(\eta_{01} s_{1}^{2} + \eta_{11} t_{1}^{2} + \eta_{12} n_{1}^{2}\right)\right) & 2 n_{0} \left(\eta_{33} n_{2} t_{0} t_{2} + \eta_{44} n_{2} s_{0} s_{2} + n_{0} \left(\eta_{02} s_{2}^{2} + \eta_{12} t_{2}^{2} + \eta_{22} n_{2}^{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{0} n_{2} s_{2} + \eta_{55} s_{2} t_{0} t_{2} + s_{0} \left(\eta_{00} s_{2}^{2} + \eta_{01} t_{2}^{2} + \eta_{02} n_{2}^{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{0} n_{2} t_{2} + \eta_{55} s_{0} s_{2} t_{2} + t_{0} \left(\eta_{01} s_{2}^{2} + \eta_{11} t_{2}^{2} + \eta_{12} n_{2}^{2}\right)\right) & \sqrt{2} \left(n_{0} \left(\eta_{33} t_{0} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{44} s_{0} \left(n_{1} s_{2} + n_{2} s_{1}\right) + 2 n_{0} \left(\eta_{02} s_{1} s_{2} + \eta_{12} t_{1} t_{2} + \eta_{22} n_{1} n_{2}\right)\right) + s_{0} \left(\eta_{44} n_{0} \left(n_{1} s_{2} + n_{2} s_{1}\right) + \eta_{55} t_{0} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 s_{0} \left(\eta_{00} s_{1} s_{2} + \eta_{01} t_{1} t_{2} + \eta_{02} n_{1} n_{2}\right)\right) + t_{0} \left(\eta_{33} n_{0} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{55} s_{0} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 t_{0} \left(\eta_{01} s_{1} s_{2} + \eta_{11} t_{1} t_{2} + \eta_{12} n_{1} n_{2}\right)\right)\right) & \sqrt{2} \left(n_{0} \left(\eta_{33} t_{0} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{44} s_{0} \left(n_{0} s_{2} + n_{2} s_{0}\right) + 2 n_{0} \left(\eta_{02} s_{0} s_{2} + \eta_{12} t_{0} t_{2} + \eta_{22} n_{0} n_{2}\right)\right) + s_{0} \left(\eta_{44} n_{0} \left(n_{0} s_{2} + n_{2} s_{0}\right) + \eta_{55} t_{0} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 s_{0} \left(\eta_{00} s_{0} s_{2} + \eta_{01} t_{0} t_{2} + \eta_{02} n_{0} n_{2}\right)\right) + t_{0} \left(\eta_{33} n_{0} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{55} s_{0} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 t_{0} \left(\eta_{01} s_{0} s_{2} + \eta_{11} t_{0} t_{2} + \eta_{12} n_{0} n_{2}\right)\right)\right) & \sqrt{2} \left(n_{0} \left(\eta_{33} t_{0} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{44} s_{0} \left(n_{0} s_{1} + n_{1} s_{0}\right) + 2 n_{0} \left(\eta_{02} s_{0} s_{1} + \eta_{12} t_{0} t_{1} + \eta_{22} n_{0} n_{1}\right)\right) + s_{0} \left(\eta_{44} n_{0} \left(n_{0} s_{1} + n_{1} s_{0}\right) + \eta_{55} t_{0} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 s_{0} \left(\eta_{00} s_{0} s_{1} + \eta_{01} t_{0} t_{1} + \eta_{02} n_{0} n_{1}\right)\right) + t_{0} \left(\eta_{33} n_{0} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{55} s_{0} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 t_{0} \left(\eta_{01} s_{0} s_{1} + \eta_{11} t_{0} t_{1} + \eta_{12} n_{0} n_{1}\right)\right)\right)\\2 n_{1} \left(\eta_{33} n_{0} t_{0} t_{1} + \eta_{44} n_{0} s_{0} s_{1} + n_{1} \left(\eta_{02} s_{0}^{2} + \eta_{12} t_{0}^{2} + \eta_{22} n_{0}^{2}\right)\right) + 2 s_{1} \left(\eta_{44} n_{0} n_{1} s_{0} + \eta_{55} s_{0} t_{0} t_{1} + s_{1} \left(\eta_{00} s_{0}^{2} + \eta_{01} t_{0}^{2} + \eta_{02} n_{0}^{2}\right)\right) + 2 t_{1} \left(\eta_{33} n_{0} n_{1} t_{0} + \eta_{55} s_{0} s_{1} t_{0} + t_{1} \left(\eta_{01} s_{0}^{2} + \eta_{11} t_{0}^{2} + \eta_{12} n_{0}^{2}\right)\right) & 2 \eta_{00} s_{1}^{4} + 4 \eta_{01} s_{1}^{2} t_{1}^{2} + 4 \eta_{02} n_{1}^{2} s_{1}^{2} + 2 \eta_{11} t_{1}^{4} + 4 \eta_{12} n_{1}^{2} t_{1}^{2} + 2 \eta_{22} n_{1}^{4} + 4 \eta_{33} n_{1}^{2} t_{1}^{2} + 4 \eta_{44} n_{1}^{2} s_{1}^{2} + 4 \eta_{55} s_{1}^{2} t_{1}^{2} & 2 n_{1} \left(\eta_{33} n_{2} t_{1} t_{2} + \eta_{44} n_{2} s_{1} s_{2} + n_{1} \left(\eta_{02} s_{2}^{2} + \eta_{12} t_{2}^{2} + \eta_{22} n_{2}^{2}\right)\right) + 2 s_{1} \left(\eta_{44} n_{1} n_{2} s_{2} + \eta_{55} s_{2} t_{1} t_{2} + s_{1} \left(\eta_{00} s_{2}^{2} + \eta_{01} t_{2}^{2} + \eta_{02} n_{2}^{2}\right)\right) + 2 t_{1} \left(\eta_{33} n_{1} n_{2} t_{2} + \eta_{55} s_{1} s_{2} t_{2} + t_{1} \left(\eta_{01} s_{2}^{2} + \eta_{11} t_{2}^{2} + \eta_{12} n_{2}^{2}\right)\right) & \sqrt{2} \left(n_{1} \left(\eta_{33} t_{1} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{44} s_{1} \left(n_{1} s_{2} + n_{2} s_{1}\right) + 2 n_{1} \left(\eta_{02} s_{1} s_{2} + \eta_{12} t_{1} t_{2} + \eta_{22} n_{1} n_{2}\right)\right) + s_{1} \left(\eta_{44} n_{1} \left(n_{1} s_{2} + n_{2} s_{1}\right) + \eta_{55} t_{1} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 s_{1} \left(\eta_{00} s_{1} s_{2} + \eta_{01} t_{1} t_{2} + \eta_{02} n_{1} n_{2}\right)\right) + t_{1} \left(\eta_{33} n_{1} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{55} s_{1} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 t_{1} \left(\eta_{01} s_{1} s_{2} + \eta_{11} t_{1} t_{2} + \eta_{12} n_{1} n_{2}\right)\right)\right) & \sqrt{2} \left(n_{1} \left(\eta_{33} t_{1} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{44} s_{1} \left(n_{0} s_{2} + n_{2} s_{0}\right) + 2 n_{1} \left(\eta_{02} s_{0} s_{2} + \eta_{12} t_{0} t_{2} + \eta_{22} n_{0} n_{2}\right)\right) + s_{1} \left(\eta_{44} n_{1} \left(n_{0} s_{2} + n_{2} s_{0}\right) + \eta_{55} t_{1} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 s_{1} \left(\eta_{00} s_{0} s_{2} + \eta_{01} t_{0} t_{2} + \eta_{02} n_{0} n_{2}\right)\right) + t_{1} \left(\eta_{33} n_{1} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{55} s_{1} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 t_{1} \left(\eta_{01} s_{0} s_{2} + \eta_{11} t_{0} t_{2} + \eta_{12} n_{0} n_{2}\right)\right)\right) & \sqrt{2} \left(n_{1} \left(\eta_{33} t_{1} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{44} s_{1} \left(n_{0} s_{1} + n_{1} s_{0}\right) + 2 n_{1} \left(\eta_{02} s_{0} s_{1} + \eta_{12} t_{0} t_{1} + \eta_{22} n_{0} n_{1}\right)\right) + s_{1} \left(\eta_{44} n_{1} \left(n_{0} s_{1} + n_{1} s_{0}\right) + \eta_{55} t_{1} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 s_{1} \left(\eta_{00} s_{0} s_{1} + \eta_{01} t_{0} t_{1} + \eta_{02} n_{0} n_{1}\right)\right) + t_{1} \left(\eta_{33} n_{1} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{55} s_{1} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 t_{1} \left(\eta_{01} s_{0} s_{1} + \eta_{11} t_{0} t_{1} + \eta_{12} n_{0} n_{1}\right)\right)\right)\\2 n_{2} \left(\eta_{33} n_{0} t_{0} t_{2} + \eta_{44} n_{0} s_{0} s_{2} + n_{2} \left(\eta_{02} s_{0}^{2} + \eta_{12} t_{0}^{2} + \eta_{22} n_{0}^{2}\right)\right) + 2 s_{2} \left(\eta_{44} n_{0} n_{2} s_{0} + \eta_{55} s_{0} t_{0} t_{2} + s_{2} \left(\eta_{00} s_{0}^{2} + \eta_{01} t_{0}^{2} + \eta_{02} n_{0}^{2}\right)\right) + 2 t_{2} \left(\eta_{33} n_{0} n_{2} t_{0} + \eta_{55} s_{0} s_{2} t_{0} + t_{2} \left(\eta_{01} s_{0}^{2} + \eta_{11} t_{0}^{2} + \eta_{12} n_{0}^{2}\right)\right) & 2 n_{2} \left(\eta_{33} n_{1} t_{1} t_{2} + \eta_{44} n_{1} s_{1} s_{2} + n_{2} \left(\eta_{02} s_{1}^{2} + \eta_{12} t_{1}^{2} + \eta_{22} n_{1}^{2}\right)\right) + 2 s_{2} \left(\eta_{44} n_{1} n_{2} s_{1} + \eta_{55} s_{1} t_{1} t_{2} + s_{2} \left(\eta_{00} s_{1}^{2} + \eta_{01} t_{1}^{2} + \eta_{02} n_{1}^{2}\right)\right) + 2 t_{2} \left(\eta_{33} n_{1} n_{2} t_{1} + \eta_{55} s_{1} s_{2} t_{1} + t_{2} \left(\eta_{01} s_{1}^{2} + \eta_{11} t_{1}^{2} + \eta_{12} n_{1}^{2}\right)\right) & 2 \eta_{00} s_{2}^{4} + 4 \eta_{01} s_{2}^{2} t_{2}^{2} + 4 \eta_{02} n_{2}^{2} s_{2}^{2} + 2 \eta_{11} t_{2}^{4} + 4 \eta_{12} n_{2}^{2} t_{2}^{2} + 2 \eta_{22} n_{2}^{4} + 4 \eta_{33} n_{2}^{2} t_{2}^{2} + 4 \eta_{44} n_{2}^{2} s_{2}^{2} + 4 \eta_{55} s_{2}^{2} t_{2}^{2} & \sqrt{2} \left(n_{2} \left(\eta_{33} t_{2} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{44} s_{2} \left(n_{1} s_{2} + n_{2} s_{1}\right) + 2 n_{2} \left(\eta_{02} s_{1} s_{2} + \eta_{12} t_{1} t_{2} + \eta_{22} n_{1} n_{2}\right)\right) + s_{2} \left(\eta_{44} n_{2} \left(n_{1} s_{2} + n_{2} s_{1}\right) + \eta_{55} t_{2} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 s_{2} \left(\eta_{00} s_{1} s_{2} + \eta_{01} t_{1} t_{2} + \eta_{02} n_{1} n_{2}\right)\right) + t_{2} \left(\eta_{33} n_{2} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{55} s_{2} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 t_{2} \left(\eta_{01} s_{1} s_{2} + \eta_{11} t_{1} t_{2} + \eta_{12} n_{1} n_{2}\right)\right)\right) & \sqrt{2} \left(n_{2} \left(\eta_{33} t_{2} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{44} s_{2} \left(n_{0} s_{2} + n_{2} s_{0}\right) + 2 n_{2} \left(\eta_{02} s_{0} s_{2} + \eta_{12} t_{0} t_{2} + \eta_{22} n_{0} n_{2}\right)\right) + s_{2} \left(\eta_{44} n_{2} \left(n_{0} s_{2} + n_{2} s_{0}\right) + \eta_{55} t_{2} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 s_{2} \left(\eta_{00} s_{0} s_{2} + \eta_{01} t_{0} t_{2} + \eta_{02} n_{0} n_{2}\right)\right) + t_{2} \left(\eta_{33} n_{2} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{55} s_{2} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 t_{2} \left(\eta_{01} s_{0} s_{2} + \eta_{11} t_{0} t_{2} + \eta_{12} n_{0} n_{2}\right)\right)\right) & \sqrt{2} \left(n_{2} \left(\eta_{33} t_{2} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{44} s_{2} \left(n_{0} s_{1} + n_{1} s_{0}\right) + 2 n_{2} \left(\eta_{02} s_{0} s_{1} + \eta_{12} t_{0} t_{1} + \eta_{22} n_{0} n_{1}\right)\right) + s_{2} \left(\eta_{44} n_{2} \left(n_{0} s_{1} + n_{1} s_{0}\right) + \eta_{55} t_{2} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 s_{2} \left(\eta_{00} s_{0} s_{1} + \eta_{01} t_{0} t_{1} + \eta_{02} n_{0} n_{1}\right)\right) + t_{2} \left(\eta_{33} n_{2} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{55} s_{2} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 t_{2} \left(\eta_{01} s_{0} s_{1} + \eta_{11} t_{0} t_{1} + \eta_{12} n_{0} n_{1}\right)\right)\right)\\\sqrt{2} \cdot \left(2 n_{1} \left(\eta_{33} n_{0} t_{0} t_{2} + \eta_{44} n_{0} s_{0} s_{2} + n_{2} \left(\eta_{02} s_{0}^{2} + \eta_{12} t_{0}^{2} + \eta_{22} n_{0}^{2}\right)\right) + 2 s_{1} \left(\eta_{44} n_{0} n_{2} s_{0} + \eta_{55} s_{0} t_{0} t_{2} + s_{2} \left(\eta_{00} s_{0}^{2} + \eta_{01} t_{0}^{2} + \eta_{02} n_{0}^{2}\right)\right) + 2 t_{1} \left(\eta_{33} n_{0} n_{2} t_{0} + \eta_{55} s_{0} s_{2} t_{0} + t_{2} \left(\eta_{01} s_{0}^{2} + \eta_{11} t_{0}^{2} + \eta_{12} n_{0}^{2}\right)\right)\right) & \sqrt{2} \cdot \left(2 n_{1} \left(\eta_{33} n_{1} t_{1} t_{2} + \eta_{44} n_{1} s_{1} s_{2} + n_{2} \left(\eta_{02} s_{1}^{2} + \eta_{12} t_{1}^{2} + \eta_{22} n_{1}^{2}\right)\right) + 2 s_{1} \left(\eta_{44} n_{1} n_{2} s_{1} + \eta_{55} s_{1} t_{1} t_{2} + s_{2} \left(\eta_{00} s_{1}^{2} + \eta_{01} t_{1}^{2} + \eta_{02} n_{1}^{2}\right)\right) + 2 t_{1} \left(\eta_{33} n_{1} n_{2} t_{1} + \eta_{55} s_{1} s_{2} t_{1} + t_{2} \left(\eta_{01} s_{1}^{2} + \eta_{11} t_{1}^{2} + \eta_{12} n_{1}^{2}\right)\right)\right) & \sqrt{2} \cdot \left(2 n_{1} n_{2} \left(\eta_{02} s_{2}^{2} + \eta_{12} t_{2}^{2} + \eta_{22} n_{2}^{2} + \eta_{33} t_{2}^{2} + \eta_{44} s_{2}^{2}\right) + 2 s_{1} s_{2} \left(\eta_{00} s_{2}^{2} + \eta_{01} t_{2}^{2} + \eta_{02} n_{2}^{2} + \eta_{44} n_{2}^{2} + \eta_{55} t_{2}^{2}\right) + 2 t_{1} t_{2} \left(\eta_{01} s_{2}^{2} + \eta_{11} t_{2}^{2} + \eta_{12} n_{2}^{2} + \eta_{33} n_{2}^{2} + \eta_{55} s_{2}^{2}\right)\right) & 2 n_{1} \left(\eta_{33} t_{2} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{44} s_{2} \left(n_{1} s_{2} + n_{2} s_{1}\right) + 2 n_{2} \left(\eta_{02} s_{1} s_{2} + \eta_{12} t_{1} t_{2} + \eta_{22} n_{1} n_{2}\right)\right) + 2 s_{1} \left(\eta_{44} n_{2} \left(n_{1} s_{2} + n_{2} s_{1}\right) + \eta_{55} t_{2} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 s_{2} \left(\eta_{00} s_{1} s_{2} + \eta_{01} t_{1} t_{2} + \eta_{02} n_{1} n_{2}\right)\right) + 2 t_{1} \left(\eta_{33} n_{2} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{55} s_{2} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 t_{2} \left(\eta_{01} s_{1} s_{2} + \eta_{11} t_{1} t_{2} + \eta_{12} n_{1} n_{2}\right)\right) & 2 n_{1} \left(\eta_{33} t_{2} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{44} s_{2} \left(n_{0} s_{2} + n_{2} s_{0}\right) + 2 n_{2} \left(\eta_{02} s_{0} s_{2} + \eta_{12} t_{0} t_{2} + \eta_{22} n_{0} n_{2}\right)\right) + 2 s_{1} \left(\eta_{44} n_{2} \left(n_{0} s_{2} + n_{2} s_{0}\right) + \eta_{55} t_{2} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 s_{2} \left(\eta_{00} s_{0} s_{2} + \eta_{01} t_{0} t_{2} + \eta_{02} n_{0} n_{2}\right)\right) + 2 t_{1} \left(\eta_{33} n_{2} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{55} s_{2} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 t_{2} \left(\eta_{01} s_{0} s_{2} + \eta_{11} t_{0} t_{2} + \eta_{12} n_{0} n_{2}\right)\right) & 2 n_{1} \left(\eta_{33} t_{2} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{44} s_{2} \left(n_{0} s_{1} + n_{1} s_{0}\right) + 2 n_{2} \left(\eta_{02} s_{0} s_{1} + \eta_{12} t_{0} t_{1} + \eta_{22} n_{0} n_{1}\right)\right) + 2 s_{1} \left(\eta_{44} n_{2} \left(n_{0} s_{1} + n_{1} s_{0}\right) + \eta_{55} t_{2} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 s_{2} \left(\eta_{00} s_{0} s_{1} + \eta_{01} t_{0} t_{1} + \eta_{02} n_{0} n_{1}\right)\right) + 2 t_{1} \left(\eta_{33} n_{2} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{55} s_{2} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 t_{2} \left(\eta_{01} s_{0} s_{1} + \eta_{11} t_{0} t_{1} + \eta_{12} n_{0} n_{1}\right)\right)\\\sqrt{2} \cdot \left(2 n_{0} \left(\eta_{33} n_{0} t_{0} t_{2} + \eta_{44} n_{0} s_{0} s_{2} + n_{2} \left(\eta_{02} s_{0}^{2} + \eta_{12} t_{0}^{2} + \eta_{22} n_{0}^{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{0} n_{2} s_{0} + \eta_{55} s_{0} t_{0} t_{2} + s_{2} \left(\eta_{00} s_{0}^{2} + \eta_{01} t_{0}^{2} + \eta_{02} n_{0}^{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{0} n_{2} t_{0} + \eta_{55} s_{0} s_{2} t_{0} + t_{2} \left(\eta_{01} s_{0}^{2} + \eta_{11} t_{0}^{2} + \eta_{12} n_{0}^{2}\right)\right)\right) & \sqrt{2} \cdot \left(2 n_{0} \left(\eta_{33} n_{1} t_{1} t_{2} + \eta_{44} n_{1} s_{1} s_{2} + n_{2} \left(\eta_{02} s_{1}^{2} + \eta_{12} t_{1}^{2} + \eta_{22} n_{1}^{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{1} n_{2} s_{1} + \eta_{55} s_{1} t_{1} t_{2} + s_{2} \left(\eta_{00} s_{1}^{2} + \eta_{01} t_{1}^{2} + \eta_{02} n_{1}^{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{1} n_{2} t_{1} + \eta_{55} s_{1} s_{2} t_{1} + t_{2} \left(\eta_{01} s_{1}^{2} + \eta_{11} t_{1}^{2} + \eta_{12} n_{1}^{2}\right)\right)\right) & \sqrt{2} \cdot \left(2 n_{0} n_{2} \left(\eta_{02} s_{2}^{2} + \eta_{12} t_{2}^{2} + \eta_{22} n_{2}^{2} + \eta_{33} t_{2}^{2} + \eta_{44} s_{2}^{2}\right) + 2 s_{0} s_{2} \left(\eta_{00} s_{2}^{2} + \eta_{01} t_{2}^{2} + \eta_{02} n_{2}^{2} + \eta_{44} n_{2}^{2} + \eta_{55} t_{2}^{2}\right) + 2 t_{0} t_{2} \left(\eta_{01} s_{2}^{2} + \eta_{11} t_{2}^{2} + \eta_{12} n_{2}^{2} + \eta_{33} n_{2}^{2} + \eta_{55} s_{2}^{2}\right)\right) & 2 n_{0} \left(\eta_{33} t_{2} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{44} s_{2} \left(n_{1} s_{2} + n_{2} s_{1}\right) + 2 n_{2} \left(\eta_{02} s_{1} s_{2} + \eta_{12} t_{1} t_{2} + \eta_{22} n_{1} n_{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{2} \left(n_{1} s_{2} + n_{2} s_{1}\right) + \eta_{55} t_{2} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 s_{2} \left(\eta_{00} s_{1} s_{2} + \eta_{01} t_{1} t_{2} + \eta_{02} n_{1} n_{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{2} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{55} s_{2} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 t_{2} \left(\eta_{01} s_{1} s_{2} + \eta_{11} t_{1} t_{2} + \eta_{12} n_{1} n_{2}\right)\right) & 2 n_{0} \left(\eta_{33} t_{2} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{44} s_{2} \left(n_{0} s_{2} + n_{2} s_{0}\right) + 2 n_{2} \left(\eta_{02} s_{0} s_{2} + \eta_{12} t_{0} t_{2} + \eta_{22} n_{0} n_{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{2} \left(n_{0} s_{2} + n_{2} s_{0}\right) + \eta_{55} t_{2} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 s_{2} \left(\eta_{00} s_{0} s_{2} + \eta_{01} t_{0} t_{2} + \eta_{02} n_{0} n_{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{2} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{55} s_{2} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 t_{2} \left(\eta_{01} s_{0} s_{2} + \eta_{11} t_{0} t_{2} + \eta_{12} n_{0} n_{2}\right)\right) & 2 n_{0} \left(\eta_{33} t_{2} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{44} s_{2} \left(n_{0} s_{1} + n_{1} s_{0}\right) + 2 n_{2} \left(\eta_{02} s_{0} s_{1} + \eta_{12} t_{0} t_{1} + \eta_{22} n_{0} n_{1}\right)\right) + 2 s_{0} \left(\eta_{44} n_{2} \left(n_{0} s_{1} + n_{1} s_{0}\right) + \eta_{55} t_{2} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 s_{2} \left(\eta_{00} s_{0} s_{1} + \eta_{01} t_{0} t_{1} + \eta_{02} n_{0} n_{1}\right)\right) + 2 t_{0} \left(\eta_{33} n_{2} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{55} s_{2} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 t_{2} \left(\eta_{01} s_{0} s_{1} + \eta_{11} t_{0} t_{1} + \eta_{12} n_{0} n_{1}\right)\right)\\\sqrt{2} \cdot \left(2 n_{0} \left(\eta_{33} n_{0} t_{0} t_{1} + \eta_{44} n_{0} s_{0} s_{1} + n_{1} \left(\eta_{02} s_{0}^{2} + \eta_{12} t_{0}^{2} + \eta_{22} n_{0}^{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{0} n_{1} s_{0} + \eta_{55} s_{0} t_{0} t_{1} + s_{1} \left(\eta_{00} s_{0}^{2} + \eta_{01} t_{0}^{2} + \eta_{02} n_{0}^{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{0} n_{1} t_{0} + \eta_{55} s_{0} s_{1} t_{0} + t_{1} \left(\eta_{01} s_{0}^{2} + \eta_{11} t_{0}^{2} + \eta_{12} n_{0}^{2}\right)\right)\right) & \sqrt{2} \cdot \left(2 n_{0} n_{1} \left(\eta_{02} s_{1}^{2} + \eta_{12} t_{1}^{2} + \eta_{22} n_{1}^{2} + \eta_{33} t_{1}^{2} + \eta_{44} s_{1}^{2}\right) + 2 s_{0} s_{1} \left(\eta_{00} s_{1}^{2} + \eta_{01} t_{1}^{2} + \eta_{02} n_{1}^{2} + \eta_{44} n_{1}^{2} + \eta_{55} t_{1}^{2}\right) + 2 t_{0} t_{1} \left(\eta_{01} s_{1}^{2} + \eta_{11} t_{1}^{2} + \eta_{12} n_{1}^{2} + \eta_{33} n_{1}^{2} + \eta_{55} s_{1}^{2}\right)\right) & \sqrt{2} \cdot \left(2 n_{0} \left(\eta_{33} n_{2} t_{1} t_{2} + \eta_{44} n_{2} s_{1} s_{2} + n_{1} \left(\eta_{02} s_{2}^{2} + \eta_{12} t_{2}^{2} + \eta_{22} n_{2}^{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{1} n_{2} s_{2} + \eta_{55} s_{2} t_{1} t_{2} + s_{1} \left(\eta_{00} s_{2}^{2} + \eta_{01} t_{2}^{2} + \eta_{02} n_{2}^{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{1} n_{2} t_{2} + \eta_{55} s_{1} s_{2} t_{2} + t_{1} \left(\eta_{01} s_{2}^{2} + \eta_{11} t_{2}^{2} + \eta_{12} n_{2}^{2}\right)\right)\right) & 2 n_{0} \left(\eta_{33} t_{1} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{44} s_{1} \left(n_{1} s_{2} + n_{2} s_{1}\right) + 2 n_{1} \left(\eta_{02} s_{1} s_{2} + \eta_{12} t_{1} t_{2} + \eta_{22} n_{1} n_{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{1} \left(n_{1} s_{2} + n_{2} s_{1}\right) + \eta_{55} t_{1} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 s_{1} \left(\eta_{00} s_{1} s_{2} + \eta_{01} t_{1} t_{2} + \eta_{02} n_{1} n_{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{1} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{55} s_{1} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 t_{1} \left(\eta_{01} s_{1} s_{2} + \eta_{11} t_{1} t_{2} + \eta_{12} n_{1} n_{2}\right)\right) & 2 n_{0} \left(\eta_{33} t_{1} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{44} s_{1} \left(n_{0} s_{2} + n_{2} s_{0}\right) + 2 n_{1} \left(\eta_{02} s_{0} s_{2} + \eta_{12} t_{0} t_{2} + \eta_{22} n_{0} n_{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{1} \left(n_{0} s_{2} + n_{2} s_{0}\right) + \eta_{55} t_{1} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 s_{1} \left(\eta_{00} s_{0} s_{2} + \eta_{01} t_{0} t_{2} + \eta_{02} n_{0} n_{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{1} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{55} s_{1} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 t_{1} \left(\eta_{01} s_{0} s_{2} + \eta_{11} t_{0} t_{2} + \eta_{12} n_{0} n_{2}\right)\right) & 2 n_{0} \left(\eta_{33} t_{1} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{44} s_{1} \left(n_{0} s_{1} + n_{1} s_{0}\right) + 2 n_{1} \left(\eta_{02} s_{0} s_{1} + \eta_{12} t_{0} t_{1} + \eta_{22} n_{0} n_{1}\right)\right) + 2 s_{0} \left(\eta_{44} n_{1} \left(n_{0} s_{1} + n_{1} s_{0}\right) + \eta_{55} t_{1} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 s_{1} \left(\eta_{00} s_{0} s_{1} + \eta_{01} t_{0} t_{1} + \eta_{02} n_{0} n_{1}\right)\right) + 2 t_{0} \left(\eta_{33} n_{1} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{55} s_{1} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 t_{1} \left(\eta_{01} s_{0} s_{1} + \eta_{11} t_{0} t_{1} + \eta_{12} n_{0} n_{1}\right)\right)\end{matrix}\right]\end{split}\]
+

In the Browaeys formulation, the orthorhombic part has two unique values (in the Voigt notation this gives three unique entries in the \(C_{IJ}\) matrix). The canonical (Mandel) form is

+
+\[\begin{split}\left[\begin{matrix}2 \xi_{0} & 0 & 2 \xi_{1} & 0 & 0 & 0\\0 & - 2 \xi_{0} & - 2 \xi_{1} & 0 & 0 & 0\\2 \xi_{1} & - 2 \xi_{1} & 0 & 0 & 0 & 0\\0 & 0 & 0 & - 2 \xi_{1} & 0 & 0\\0 & 0 & 0 & 0 & 2 \xi_{1} & 0\\0 & 0 & 0 & 0 & 0 & 0\end{matrix}\right]\end{split}\]
+

The rotated form:

+
+\[\begin{split}\left[\begin{matrix}2 \eta_{00} s_{0}^{4} + 4 \eta_{01} s_{0}^{2} t_{0}^{2} + 4 \eta_{02} n_{0}^{2} s_{0}^{2} + 2 \eta_{11} t_{0}^{4} + 4 \eta_{12} n_{0}^{2} t_{0}^{2} + 2 \eta_{22} n_{0}^{4} + 4 \eta_{33} n_{0}^{2} t_{0}^{2} + 4 \eta_{44} n_{0}^{2} s_{0}^{2} + 4 \eta_{55} s_{0}^{2} t_{0}^{2} & 2 n_{0} \left(\eta_{33} n_{1} t_{0} t_{1} + \eta_{44} n_{1} s_{0} s_{1} + n_{0} \left(\eta_{02} s_{1}^{2} + \eta_{12} t_{1}^{2} + \eta_{22} n_{1}^{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{0} n_{1} s_{1} + \eta_{55} s_{1} t_{0} t_{1} + s_{0} \left(\eta_{00} s_{1}^{2} + \eta_{01} t_{1}^{2} + \eta_{02} n_{1}^{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{0} n_{1} t_{1} + \eta_{55} s_{0} s_{1} t_{1} + t_{0} \left(\eta_{01} s_{1}^{2} + \eta_{11} t_{1}^{2} + \eta_{12} n_{1}^{2}\right)\right) & 2 n_{0} \left(\eta_{33} n_{2} t_{0} t_{2} + \eta_{44} n_{2} s_{0} s_{2} + n_{0} \left(\eta_{02} s_{2}^{2} + \eta_{12} t_{2}^{2} + \eta_{22} n_{2}^{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{0} n_{2} s_{2} + \eta_{55} s_{2} t_{0} t_{2} + s_{0} \left(\eta_{00} s_{2}^{2} + \eta_{01} t_{2}^{2} + \eta_{02} n_{2}^{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{0} n_{2} t_{2} + \eta_{55} s_{0} s_{2} t_{2} + t_{0} \left(\eta_{01} s_{2}^{2} + \eta_{11} t_{2}^{2} + \eta_{12} n_{2}^{2}\right)\right) & \sqrt{2} \left(n_{0} \left(\eta_{33} t_{0} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{44} s_{0} \left(n_{1} s_{2} + n_{2} s_{1}\right) + 2 n_{0} \left(\eta_{02} s_{1} s_{2} + \eta_{12} t_{1} t_{2} + \eta_{22} n_{1} n_{2}\right)\right) + s_{0} \left(\eta_{44} n_{0} \left(n_{1} s_{2} + n_{2} s_{1}\right) + \eta_{55} t_{0} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 s_{0} \left(\eta_{00} s_{1} s_{2} + \eta_{01} t_{1} t_{2} + \eta_{02} n_{1} n_{2}\right)\right) + t_{0} \left(\eta_{33} n_{0} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{55} s_{0} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 t_{0} \left(\eta_{01} s_{1} s_{2} + \eta_{11} t_{1} t_{2} + \eta_{12} n_{1} n_{2}\right)\right)\right) & \sqrt{2} \left(n_{0} \left(\eta_{33} t_{0} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{44} s_{0} \left(n_{0} s_{2} + n_{2} s_{0}\right) + 2 n_{0} \left(\eta_{02} s_{0} s_{2} + \eta_{12} t_{0} t_{2} + \eta_{22} n_{0} n_{2}\right)\right) + s_{0} \left(\eta_{44} n_{0} \left(n_{0} s_{2} + n_{2} s_{0}\right) + \eta_{55} t_{0} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 s_{0} \left(\eta_{00} s_{0} s_{2} + \eta_{01} t_{0} t_{2} + \eta_{02} n_{0} n_{2}\right)\right) + t_{0} \left(\eta_{33} n_{0} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{55} s_{0} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 t_{0} \left(\eta_{01} s_{0} s_{2} + \eta_{11} t_{0} t_{2} + \eta_{12} n_{0} n_{2}\right)\right)\right) & \sqrt{2} \left(n_{0} \left(\eta_{33} t_{0} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{44} s_{0} \left(n_{0} s_{1} + n_{1} s_{0}\right) + 2 n_{0} \left(\eta_{02} s_{0} s_{1} + \eta_{12} t_{0} t_{1} + \eta_{22} n_{0} n_{1}\right)\right) + s_{0} \left(\eta_{44} n_{0} \left(n_{0} s_{1} + n_{1} s_{0}\right) + \eta_{55} t_{0} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 s_{0} \left(\eta_{00} s_{0} s_{1} + \eta_{01} t_{0} t_{1} + \eta_{02} n_{0} n_{1}\right)\right) + t_{0} \left(\eta_{33} n_{0} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{55} s_{0} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 t_{0} \left(\eta_{01} s_{0} s_{1} + \eta_{11} t_{0} t_{1} + \eta_{12} n_{0} n_{1}\right)\right)\right)\\2 n_{1} \left(\eta_{33} n_{0} t_{0} t_{1} + \eta_{44} n_{0} s_{0} s_{1} + n_{1} \left(\eta_{02} s_{0}^{2} + \eta_{12} t_{0}^{2} + \eta_{22} n_{0}^{2}\right)\right) + 2 s_{1} \left(\eta_{44} n_{0} n_{1} s_{0} + \eta_{55} s_{0} t_{0} t_{1} + s_{1} \left(\eta_{00} s_{0}^{2} + \eta_{01} t_{0}^{2} + \eta_{02} n_{0}^{2}\right)\right) + 2 t_{1} \left(\eta_{33} n_{0} n_{1} t_{0} + \eta_{55} s_{0} s_{1} t_{0} + t_{1} \left(\eta_{01} s_{0}^{2} + \eta_{11} t_{0}^{2} + \eta_{12} n_{0}^{2}\right)\right) & 2 \eta_{00} s_{1}^{4} + 4 \eta_{01} s_{1}^{2} t_{1}^{2} + 4 \eta_{02} n_{1}^{2} s_{1}^{2} + 2 \eta_{11} t_{1}^{4} + 4 \eta_{12} n_{1}^{2} t_{1}^{2} + 2 \eta_{22} n_{1}^{4} + 4 \eta_{33} n_{1}^{2} t_{1}^{2} + 4 \eta_{44} n_{1}^{2} s_{1}^{2} + 4 \eta_{55} s_{1}^{2} t_{1}^{2} & 2 n_{1} \left(\eta_{33} n_{2} t_{1} t_{2} + \eta_{44} n_{2} s_{1} s_{2} + n_{1} \left(\eta_{02} s_{2}^{2} + \eta_{12} t_{2}^{2} + \eta_{22} n_{2}^{2}\right)\right) + 2 s_{1} \left(\eta_{44} n_{1} n_{2} s_{2} + \eta_{55} s_{2} t_{1} t_{2} + s_{1} \left(\eta_{00} s_{2}^{2} + \eta_{01} t_{2}^{2} + \eta_{02} n_{2}^{2}\right)\right) + 2 t_{1} \left(\eta_{33} n_{1} n_{2} t_{2} + \eta_{55} s_{1} s_{2} t_{2} + t_{1} \left(\eta_{01} s_{2}^{2} + \eta_{11} t_{2}^{2} + \eta_{12} n_{2}^{2}\right)\right) & \sqrt{2} \left(n_{1} \left(\eta_{33} t_{1} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{44} s_{1} \left(n_{1} s_{2} + n_{2} s_{1}\right) + 2 n_{1} \left(\eta_{02} s_{1} s_{2} + \eta_{12} t_{1} t_{2} + \eta_{22} n_{1} n_{2}\right)\right) + s_{1} \left(\eta_{44} n_{1} \left(n_{1} s_{2} + n_{2} s_{1}\right) + \eta_{55} t_{1} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 s_{1} \left(\eta_{00} s_{1} s_{2} + \eta_{01} t_{1} t_{2} + \eta_{02} n_{1} n_{2}\right)\right) + t_{1} \left(\eta_{33} n_{1} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{55} s_{1} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 t_{1} \left(\eta_{01} s_{1} s_{2} + \eta_{11} t_{1} t_{2} + \eta_{12} n_{1} n_{2}\right)\right)\right) & \sqrt{2} \left(n_{1} \left(\eta_{33} t_{1} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{44} s_{1} \left(n_{0} s_{2} + n_{2} s_{0}\right) + 2 n_{1} \left(\eta_{02} s_{0} s_{2} + \eta_{12} t_{0} t_{2} + \eta_{22} n_{0} n_{2}\right)\right) + s_{1} \left(\eta_{44} n_{1} \left(n_{0} s_{2} + n_{2} s_{0}\right) + \eta_{55} t_{1} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 s_{1} \left(\eta_{00} s_{0} s_{2} + \eta_{01} t_{0} t_{2} + \eta_{02} n_{0} n_{2}\right)\right) + t_{1} \left(\eta_{33} n_{1} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{55} s_{1} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 t_{1} \left(\eta_{01} s_{0} s_{2} + \eta_{11} t_{0} t_{2} + \eta_{12} n_{0} n_{2}\right)\right)\right) & \sqrt{2} \left(n_{1} \left(\eta_{33} t_{1} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{44} s_{1} \left(n_{0} s_{1} + n_{1} s_{0}\right) + 2 n_{1} \left(\eta_{02} s_{0} s_{1} + \eta_{12} t_{0} t_{1} + \eta_{22} n_{0} n_{1}\right)\right) + s_{1} \left(\eta_{44} n_{1} \left(n_{0} s_{1} + n_{1} s_{0}\right) + \eta_{55} t_{1} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 s_{1} \left(\eta_{00} s_{0} s_{1} + \eta_{01} t_{0} t_{1} + \eta_{02} n_{0} n_{1}\right)\right) + t_{1} \left(\eta_{33} n_{1} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{55} s_{1} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 t_{1} \left(\eta_{01} s_{0} s_{1} + \eta_{11} t_{0} t_{1} + \eta_{12} n_{0} n_{1}\right)\right)\right)\\2 n_{2} \left(\eta_{33} n_{0} t_{0} t_{2} + \eta_{44} n_{0} s_{0} s_{2} + n_{2} \left(\eta_{02} s_{0}^{2} + \eta_{12} t_{0}^{2} + \eta_{22} n_{0}^{2}\right)\right) + 2 s_{2} \left(\eta_{44} n_{0} n_{2} s_{0} + \eta_{55} s_{0} t_{0} t_{2} + s_{2} \left(\eta_{00} s_{0}^{2} + \eta_{01} t_{0}^{2} + \eta_{02} n_{0}^{2}\right)\right) + 2 t_{2} \left(\eta_{33} n_{0} n_{2} t_{0} + \eta_{55} s_{0} s_{2} t_{0} + t_{2} \left(\eta_{01} s_{0}^{2} + \eta_{11} t_{0}^{2} + \eta_{12} n_{0}^{2}\right)\right) & 2 n_{2} \left(\eta_{33} n_{1} t_{1} t_{2} + \eta_{44} n_{1} s_{1} s_{2} + n_{2} \left(\eta_{02} s_{1}^{2} + \eta_{12} t_{1}^{2} + \eta_{22} n_{1}^{2}\right)\right) + 2 s_{2} \left(\eta_{44} n_{1} n_{2} s_{1} + \eta_{55} s_{1} t_{1} t_{2} + s_{2} \left(\eta_{00} s_{1}^{2} + \eta_{01} t_{1}^{2} + \eta_{02} n_{1}^{2}\right)\right) + 2 t_{2} \left(\eta_{33} n_{1} n_{2} t_{1} + \eta_{55} s_{1} s_{2} t_{1} + t_{2} \left(\eta_{01} s_{1}^{2} + \eta_{11} t_{1}^{2} + \eta_{12} n_{1}^{2}\right)\right) & 2 \eta_{00} s_{2}^{4} + 4 \eta_{01} s_{2}^{2} t_{2}^{2} + 4 \eta_{02} n_{2}^{2} s_{2}^{2} + 2 \eta_{11} t_{2}^{4} + 4 \eta_{12} n_{2}^{2} t_{2}^{2} + 2 \eta_{22} n_{2}^{4} + 4 \eta_{33} n_{2}^{2} t_{2}^{2} + 4 \eta_{44} n_{2}^{2} s_{2}^{2} + 4 \eta_{55} s_{2}^{2} t_{2}^{2} & \sqrt{2} \left(n_{2} \left(\eta_{33} t_{2} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{44} s_{2} \left(n_{1} s_{2} + n_{2} s_{1}\right) + 2 n_{2} \left(\eta_{02} s_{1} s_{2} + \eta_{12} t_{1} t_{2} + \eta_{22} n_{1} n_{2}\right)\right) + s_{2} \left(\eta_{44} n_{2} \left(n_{1} s_{2} + n_{2} s_{1}\right) + \eta_{55} t_{2} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 s_{2} \left(\eta_{00} s_{1} s_{2} + \eta_{01} t_{1} t_{2} + \eta_{02} n_{1} n_{2}\right)\right) + t_{2} \left(\eta_{33} n_{2} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{55} s_{2} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 t_{2} \left(\eta_{01} s_{1} s_{2} + \eta_{11} t_{1} t_{2} + \eta_{12} n_{1} n_{2}\right)\right)\right) & \sqrt{2} \left(n_{2} \left(\eta_{33} t_{2} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{44} s_{2} \left(n_{0} s_{2} + n_{2} s_{0}\right) + 2 n_{2} \left(\eta_{02} s_{0} s_{2} + \eta_{12} t_{0} t_{2} + \eta_{22} n_{0} n_{2}\right)\right) + s_{2} \left(\eta_{44} n_{2} \left(n_{0} s_{2} + n_{2} s_{0}\right) + \eta_{55} t_{2} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 s_{2} \left(\eta_{00} s_{0} s_{2} + \eta_{01} t_{0} t_{2} + \eta_{02} n_{0} n_{2}\right)\right) + t_{2} \left(\eta_{33} n_{2} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{55} s_{2} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 t_{2} \left(\eta_{01} s_{0} s_{2} + \eta_{11} t_{0} t_{2} + \eta_{12} n_{0} n_{2}\right)\right)\right) & \sqrt{2} \left(n_{2} \left(\eta_{33} t_{2} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{44} s_{2} \left(n_{0} s_{1} + n_{1} s_{0}\right) + 2 n_{2} \left(\eta_{02} s_{0} s_{1} + \eta_{12} t_{0} t_{1} + \eta_{22} n_{0} n_{1}\right)\right) + s_{2} \left(\eta_{44} n_{2} \left(n_{0} s_{1} + n_{1} s_{0}\right) + \eta_{55} t_{2} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 s_{2} \left(\eta_{00} s_{0} s_{1} + \eta_{01} t_{0} t_{1} + \eta_{02} n_{0} n_{1}\right)\right) + t_{2} \left(\eta_{33} n_{2} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{55} s_{2} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 t_{2} \left(\eta_{01} s_{0} s_{1} + \eta_{11} t_{0} t_{1} + \eta_{12} n_{0} n_{1}\right)\right)\right)\\\sqrt{2} \cdot \left(2 n_{1} \left(\eta_{33} n_{0} t_{0} t_{2} + \eta_{44} n_{0} s_{0} s_{2} + n_{2} \left(\eta_{02} s_{0}^{2} + \eta_{12} t_{0}^{2} + \eta_{22} n_{0}^{2}\right)\right) + 2 s_{1} \left(\eta_{44} n_{0} n_{2} s_{0} + \eta_{55} s_{0} t_{0} t_{2} + s_{2} \left(\eta_{00} s_{0}^{2} + \eta_{01} t_{0}^{2} + \eta_{02} n_{0}^{2}\right)\right) + 2 t_{1} \left(\eta_{33} n_{0} n_{2} t_{0} + \eta_{55} s_{0} s_{2} t_{0} + t_{2} \left(\eta_{01} s_{0}^{2} + \eta_{11} t_{0}^{2} + \eta_{12} n_{0}^{2}\right)\right)\right) & \sqrt{2} \cdot \left(2 n_{1} \left(\eta_{33} n_{1} t_{1} t_{2} + \eta_{44} n_{1} s_{1} s_{2} + n_{2} \left(\eta_{02} s_{1}^{2} + \eta_{12} t_{1}^{2} + \eta_{22} n_{1}^{2}\right)\right) + 2 s_{1} \left(\eta_{44} n_{1} n_{2} s_{1} + \eta_{55} s_{1} t_{1} t_{2} + s_{2} \left(\eta_{00} s_{1}^{2} + \eta_{01} t_{1}^{2} + \eta_{02} n_{1}^{2}\right)\right) + 2 t_{1} \left(\eta_{33} n_{1} n_{2} t_{1} + \eta_{55} s_{1} s_{2} t_{1} + t_{2} \left(\eta_{01} s_{1}^{2} + \eta_{11} t_{1}^{2} + \eta_{12} n_{1}^{2}\right)\right)\right) & \sqrt{2} \cdot \left(2 n_{1} n_{2} \left(\eta_{02} s_{2}^{2} + \eta_{12} t_{2}^{2} + \eta_{22} n_{2}^{2} + \eta_{33} t_{2}^{2} + \eta_{44} s_{2}^{2}\right) + 2 s_{1} s_{2} \left(\eta_{00} s_{2}^{2} + \eta_{01} t_{2}^{2} + \eta_{02} n_{2}^{2} + \eta_{44} n_{2}^{2} + \eta_{55} t_{2}^{2}\right) + 2 t_{1} t_{2} \left(\eta_{01} s_{2}^{2} + \eta_{11} t_{2}^{2} + \eta_{12} n_{2}^{2} + \eta_{33} n_{2}^{2} + \eta_{55} s_{2}^{2}\right)\right) & 2 n_{1} \left(\eta_{33} t_{2} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{44} s_{2} \left(n_{1} s_{2} + n_{2} s_{1}\right) + 2 n_{2} \left(\eta_{02} s_{1} s_{2} + \eta_{12} t_{1} t_{2} + \eta_{22} n_{1} n_{2}\right)\right) + 2 s_{1} \left(\eta_{44} n_{2} \left(n_{1} s_{2} + n_{2} s_{1}\right) + \eta_{55} t_{2} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 s_{2} \left(\eta_{00} s_{1} s_{2} + \eta_{01} t_{1} t_{2} + \eta_{02} n_{1} n_{2}\right)\right) + 2 t_{1} \left(\eta_{33} n_{2} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{55} s_{2} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 t_{2} \left(\eta_{01} s_{1} s_{2} + \eta_{11} t_{1} t_{2} + \eta_{12} n_{1} n_{2}\right)\right) & 2 n_{1} \left(\eta_{33} t_{2} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{44} s_{2} \left(n_{0} s_{2} + n_{2} s_{0}\right) + 2 n_{2} \left(\eta_{02} s_{0} s_{2} + \eta_{12} t_{0} t_{2} + \eta_{22} n_{0} n_{2}\right)\right) + 2 s_{1} \left(\eta_{44} n_{2} \left(n_{0} s_{2} + n_{2} s_{0}\right) + \eta_{55} t_{2} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 s_{2} \left(\eta_{00} s_{0} s_{2} + \eta_{01} t_{0} t_{2} + \eta_{02} n_{0} n_{2}\right)\right) + 2 t_{1} \left(\eta_{33} n_{2} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{55} s_{2} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 t_{2} \left(\eta_{01} s_{0} s_{2} + \eta_{11} t_{0} t_{2} + \eta_{12} n_{0} n_{2}\right)\right) & 2 n_{1} \left(\eta_{33} t_{2} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{44} s_{2} \left(n_{0} s_{1} + n_{1} s_{0}\right) + 2 n_{2} \left(\eta_{02} s_{0} s_{1} + \eta_{12} t_{0} t_{1} + \eta_{22} n_{0} n_{1}\right)\right) + 2 s_{1} \left(\eta_{44} n_{2} \left(n_{0} s_{1} + n_{1} s_{0}\right) + \eta_{55} t_{2} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 s_{2} \left(\eta_{00} s_{0} s_{1} + \eta_{01} t_{0} t_{1} + \eta_{02} n_{0} n_{1}\right)\right) + 2 t_{1} \left(\eta_{33} n_{2} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{55} s_{2} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 t_{2} \left(\eta_{01} s_{0} s_{1} + \eta_{11} t_{0} t_{1} + \eta_{12} n_{0} n_{1}\right)\right)\\\sqrt{2} \cdot \left(2 n_{0} \left(\eta_{33} n_{0} t_{0} t_{2} + \eta_{44} n_{0} s_{0} s_{2} + n_{2} \left(\eta_{02} s_{0}^{2} + \eta_{12} t_{0}^{2} + \eta_{22} n_{0}^{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{0} n_{2} s_{0} + \eta_{55} s_{0} t_{0} t_{2} + s_{2} \left(\eta_{00} s_{0}^{2} + \eta_{01} t_{0}^{2} + \eta_{02} n_{0}^{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{0} n_{2} t_{0} + \eta_{55} s_{0} s_{2} t_{0} + t_{2} \left(\eta_{01} s_{0}^{2} + \eta_{11} t_{0}^{2} + \eta_{12} n_{0}^{2}\right)\right)\right) & \sqrt{2} \cdot \left(2 n_{0} \left(\eta_{33} n_{1} t_{1} t_{2} + \eta_{44} n_{1} s_{1} s_{2} + n_{2} \left(\eta_{02} s_{1}^{2} + \eta_{12} t_{1}^{2} + \eta_{22} n_{1}^{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{1} n_{2} s_{1} + \eta_{55} s_{1} t_{1} t_{2} + s_{2} \left(\eta_{00} s_{1}^{2} + \eta_{01} t_{1}^{2} + \eta_{02} n_{1}^{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{1} n_{2} t_{1} + \eta_{55} s_{1} s_{2} t_{1} + t_{2} \left(\eta_{01} s_{1}^{2} + \eta_{11} t_{1}^{2} + \eta_{12} n_{1}^{2}\right)\right)\right) & \sqrt{2} \cdot \left(2 n_{0} n_{2} \left(\eta_{02} s_{2}^{2} + \eta_{12} t_{2}^{2} + \eta_{22} n_{2}^{2} + \eta_{33} t_{2}^{2} + \eta_{44} s_{2}^{2}\right) + 2 s_{0} s_{2} \left(\eta_{00} s_{2}^{2} + \eta_{01} t_{2}^{2} + \eta_{02} n_{2}^{2} + \eta_{44} n_{2}^{2} + \eta_{55} t_{2}^{2}\right) + 2 t_{0} t_{2} \left(\eta_{01} s_{2}^{2} + \eta_{11} t_{2}^{2} + \eta_{12} n_{2}^{2} + \eta_{33} n_{2}^{2} + \eta_{55} s_{2}^{2}\right)\right) & 2 n_{0} \left(\eta_{33} t_{2} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{44} s_{2} \left(n_{1} s_{2} + n_{2} s_{1}\right) + 2 n_{2} \left(\eta_{02} s_{1} s_{2} + \eta_{12} t_{1} t_{2} + \eta_{22} n_{1} n_{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{2} \left(n_{1} s_{2} + n_{2} s_{1}\right) + \eta_{55} t_{2} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 s_{2} \left(\eta_{00} s_{1} s_{2} + \eta_{01} t_{1} t_{2} + \eta_{02} n_{1} n_{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{2} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{55} s_{2} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 t_{2} \left(\eta_{01} s_{1} s_{2} + \eta_{11} t_{1} t_{2} + \eta_{12} n_{1} n_{2}\right)\right) & 2 n_{0} \left(\eta_{33} t_{2} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{44} s_{2} \left(n_{0} s_{2} + n_{2} s_{0}\right) + 2 n_{2} \left(\eta_{02} s_{0} s_{2} + \eta_{12} t_{0} t_{2} + \eta_{22} n_{0} n_{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{2} \left(n_{0} s_{2} + n_{2} s_{0}\right) + \eta_{55} t_{2} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 s_{2} \left(\eta_{00} s_{0} s_{2} + \eta_{01} t_{0} t_{2} + \eta_{02} n_{0} n_{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{2} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{55} s_{2} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 t_{2} \left(\eta_{01} s_{0} s_{2} + \eta_{11} t_{0} t_{2} + \eta_{12} n_{0} n_{2}\right)\right) & 2 n_{0} \left(\eta_{33} t_{2} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{44} s_{2} \left(n_{0} s_{1} + n_{1} s_{0}\right) + 2 n_{2} \left(\eta_{02} s_{0} s_{1} + \eta_{12} t_{0} t_{1} + \eta_{22} n_{0} n_{1}\right)\right) + 2 s_{0} \left(\eta_{44} n_{2} \left(n_{0} s_{1} + n_{1} s_{0}\right) + \eta_{55} t_{2} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 s_{2} \left(\eta_{00} s_{0} s_{1} + \eta_{01} t_{0} t_{1} + \eta_{02} n_{0} n_{1}\right)\right) + 2 t_{0} \left(\eta_{33} n_{2} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{55} s_{2} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 t_{2} \left(\eta_{01} s_{0} s_{1} + \eta_{11} t_{0} t_{1} + \eta_{12} n_{0} n_{1}\right)\right)\\\sqrt{2} \cdot \left(2 n_{0} \left(\eta_{33} n_{0} t_{0} t_{1} + \eta_{44} n_{0} s_{0} s_{1} + n_{1} \left(\eta_{02} s_{0}^{2} + \eta_{12} t_{0}^{2} + \eta_{22} n_{0}^{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{0} n_{1} s_{0} + \eta_{55} s_{0} t_{0} t_{1} + s_{1} \left(\eta_{00} s_{0}^{2} + \eta_{01} t_{0}^{2} + \eta_{02} n_{0}^{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{0} n_{1} t_{0} + \eta_{55} s_{0} s_{1} t_{0} + t_{1} \left(\eta_{01} s_{0}^{2} + \eta_{11} t_{0}^{2} + \eta_{12} n_{0}^{2}\right)\right)\right) & \sqrt{2} \cdot \left(2 n_{0} n_{1} \left(\eta_{02} s_{1}^{2} + \eta_{12} t_{1}^{2} + \eta_{22} n_{1}^{2} + \eta_{33} t_{1}^{2} + \eta_{44} s_{1}^{2}\right) + 2 s_{0} s_{1} \left(\eta_{00} s_{1}^{2} + \eta_{01} t_{1}^{2} + \eta_{02} n_{1}^{2} + \eta_{44} n_{1}^{2} + \eta_{55} t_{1}^{2}\right) + 2 t_{0} t_{1} \left(\eta_{01} s_{1}^{2} + \eta_{11} t_{1}^{2} + \eta_{12} n_{1}^{2} + \eta_{33} n_{1}^{2} + \eta_{55} s_{1}^{2}\right)\right) & \sqrt{2} \cdot \left(2 n_{0} \left(\eta_{33} n_{2} t_{1} t_{2} + \eta_{44} n_{2} s_{1} s_{2} + n_{1} \left(\eta_{02} s_{2}^{2} + \eta_{12} t_{2}^{2} + \eta_{22} n_{2}^{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{1} n_{2} s_{2} + \eta_{55} s_{2} t_{1} t_{2} + s_{1} \left(\eta_{00} s_{2}^{2} + \eta_{01} t_{2}^{2} + \eta_{02} n_{2}^{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{1} n_{2} t_{2} + \eta_{55} s_{1} s_{2} t_{2} + t_{1} \left(\eta_{01} s_{2}^{2} + \eta_{11} t_{2}^{2} + \eta_{12} n_{2}^{2}\right)\right)\right) & 2 n_{0} \left(\eta_{33} t_{1} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{44} s_{1} \left(n_{1} s_{2} + n_{2} s_{1}\right) + 2 n_{1} \left(\eta_{02} s_{1} s_{2} + \eta_{12} t_{1} t_{2} + \eta_{22} n_{1} n_{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{1} \left(n_{1} s_{2} + n_{2} s_{1}\right) + \eta_{55} t_{1} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 s_{1} \left(\eta_{00} s_{1} s_{2} + \eta_{01} t_{1} t_{2} + \eta_{02} n_{1} n_{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{1} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{55} s_{1} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 t_{1} \left(\eta_{01} s_{1} s_{2} + \eta_{11} t_{1} t_{2} + \eta_{12} n_{1} n_{2}\right)\right) & 2 n_{0} \left(\eta_{33} t_{1} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{44} s_{1} \left(n_{0} s_{2} + n_{2} s_{0}\right) + 2 n_{1} \left(\eta_{02} s_{0} s_{2} + \eta_{12} t_{0} t_{2} + \eta_{22} n_{0} n_{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{1} \left(n_{0} s_{2} + n_{2} s_{0}\right) + \eta_{55} t_{1} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 s_{1} \left(\eta_{00} s_{0} s_{2} + \eta_{01} t_{0} t_{2} + \eta_{02} n_{0} n_{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{1} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{55} s_{1} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 t_{1} \left(\eta_{01} s_{0} s_{2} + \eta_{11} t_{0} t_{2} + \eta_{12} n_{0} n_{2}\right)\right) & 2 n_{0} \left(\eta_{33} t_{1} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{44} s_{1} \left(n_{0} s_{1} + n_{1} s_{0}\right) + 2 n_{1} \left(\eta_{02} s_{0} s_{1} + \eta_{12} t_{0} t_{1} + \eta_{22} n_{0} n_{1}\right)\right) + 2 s_{0} \left(\eta_{44} n_{1} \left(n_{0} s_{1} + n_{1} s_{0}\right) + \eta_{55} t_{1} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 s_{1} \left(\eta_{00} s_{0} s_{1} + \eta_{01} t_{0} t_{1} + \eta_{02} n_{0} n_{1}\right)\right) + 2 t_{0} \left(\eta_{33} n_{1} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{55} s_{1} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 t_{1} \left(\eta_{01} s_{0} s_{1} + \eta_{11} t_{0} t_{1} + \eta_{12} n_{0} n_{1}\right)\right)\end{matrix}\right]\end{split}\]
+
+
+
## I can't see much benefit in expanding the full orthorhombic model
+## in this way - keeping the rotations separated makes more sense.
+
+eta_00 = sympy.symbols(r"\eta_00")
+eta_11 = sympy.symbols(r"\eta_11")
+eta_22 = sympy.symbols(r"\eta_22")
+eta_33 = sympy.symbols(r"\eta_33")
+eta_44 = sympy.symbols(r"\eta_44")
+eta_55 = sympy.symbols(r"\eta_55")
+eta_01 = sympy.symbols(r"\eta_01")
+eta_02 = sympy.symbols(r"\eta_02")
+eta_12 = sympy.symbols(r"\eta_12")
+
+I_ijkl = uw.maths.tensor.rank4_identity(3) * 0
+C_IJm_ORTHO = uw.maths.tensor.rank4_to_mandel(I_ijkl, 3)
+
+C_IJm_ORTHO[0,0] = 2*eta_00
+C_IJm_ORTHO[1,1] = 2*eta_11
+C_IJm_ORTHO[2,2] = 2*eta_22
+C_IJm_ORTHO[3,3] = 2*eta_33  # yz
+C_IJm_ORTHO[4,4] = 2*eta_44  # xz
+C_IJm_ORTHO[5,5] = 2*eta_55  # xy
+C_IJm_ORTHO[0,1] = C_IJm_ORTHO[1,0] = 2*eta_01
+C_IJm_ORTHO[0,2] = C_IJm_ORTHO[2,0] = 2*eta_02
+C_IJm_ORTHO[1,2] = C_IJm_ORTHO[2,1] = 2*eta_12
+
+C_ijkl_ORTHO = uw.maths.tensor.mandel_to_rank4(C_IJm_ORTHO, 3)
+C_IJv_ORTHO = sympy.simplify(uw.maths.tensor.rank4_to_voigt(C_ijkl_ORTHO, 3))
+
+C_IJv_ORTHO
+
+
+
+
+
+
+
# Rotation: Use 3 orthogonal unit vectors to define cannonical orientation
+
+n = sympy.Matrix(sympy.symarray("n",(3,)))
+s = sympy.Matrix(sympy.symarray("s",(3,)))
+t = sympy.Matrix(sympy.symarray("t",(3,)))
+
+# This would work but is less clear in terms of notation
+# t = -mesh3.vector.cross(n,s).T # complete the coordinate triad
+
+Rx = sympy.BlockMatrix((s,t,n)).as_explicit()
+
+Rx
+
+
+
+
+
+
+
C_ijkl_ORTHO = uw.maths.tensor.mandel_to_rank4(C_IJm_ORTHO, 3)
+C_ijkl_ORTHO_R = sympy.simplify(uw.maths.tensor.tensor_rotation(Rx, C_ijkl_ORTHO))
+uw.maths.tensor.rank4_to_mandel(C_ijkl_ORTHO_R, 3)
+
+# print(sympy.latex(uw.maths.tensor.rank4_to_mandel(C_ijkl_ORTHO_R, 3)))
+
+
+
+
+
+
+

+xi_0 = sympy.symbols(r"\xi_0")
+xi_1 = sympy.symbols(r"\xi_1")
+
+I_ijkl = uw.maths.tensor.rank4_identity(3) * 0
+C_IJm_BC = uw.maths.tensor.rank4_to_mandel(I_ijkl, 3)
+
+C_IJm_BC[0,0] =  2*xi_0
+C_IJm_BC[1,1] = -2*xi_0
+C_IJm_BC[3,3] = -2*xi_1  # yz
+C_IJm_BC[4,4] = 2*xi_1  # xz
+C_IJm_BC[5,5] = 0
+C_IJm_BC[0,2] = C_IJm_BC[2,0] = 2*xi_1
+C_IJm_BC[1,2] = C_IJm_BC[2,1] = -2*xi_1
+
+C_ijkl_BC = uw.maths.tensor.mandel_to_rank4(C_IJm_BC, 3)
+C_IJv_BC = sympy.simplify(uw.maths.tensor.rank4_to_voigt(C_ijkl_BC, 3))
+
+print(sympy.latex(C_IJm_BC))
+
+C_IJv_BC
+
+# Rotation by Rx
+
+C_ijkl_BC = uw.maths.tensor.mandel_to_rank4(C_IJm_BC, 3)
+C_ijkl_BC_R = sympy.simplify(uw.maths.tensor.tensor_rotation(Rx, C_ijkl_ORTHO))
+uw.maths.tensor.rank4_to_mandel(C_ijkl_BC_R, 3)
+
+print(sympy.latex(uw.maths.tensor.rank4_to_mandel(C_ijkl_BC_R, 3)))
+
+
+
+
+
+
+
display(C_ijkl_BC_R)
+
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Utilities/Ex_BCs_from_meshVariables.html b/main/Notebooks/Examples-Utilities/Ex_BCs_from_meshVariables.html new file mode 100644 index 0000000..27beebb --- /dev/null +++ b/main/Notebooks/Examples-Utilities/Ex_BCs_from_meshVariables.html @@ -0,0 +1,808 @@ + + + + + + + + + + + Cylindrical 2D Diffusion — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Cylindrical 2D Diffusion

+ +
+
+ +
+
+
+ + + + +
+ +
+

Cylindrical 2D Diffusion#

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
from petsc4py import PETSc
+import underworld3 as uw
+from underworld3.systems import Poisson
+import numpy as np
+
+options = PETSc.Options()
+
+options["dm_plex_check_all"] = None
+options["poisson_ksp_rtol"] = 1.0e-3
+# options["poisson_ksp_monitor_short"] = None
+# options["poisson_nes_type"]  = "fas"
+options["poisson_snes_converged_reason"] = None
+options["poisson_snes_monitor_short"] = None
+# options["poisson_snes_view"]=None
+options["poisson_snes_rtol"] = 1.0e-3
+
+import os
+
+os.environ["SYMPY_USE_CACHE"] = "no"
+
+
+
+
+
+
+
# Set some things
+k = 1.0
+h = 5.0
+t_i = 2.0
+t_o = 1.0
+r_i = 0.5
+r_o = 1.0
+
+
+
+
+
+
+
dim = 2
+radius_inner = 0.1
+radius_outer = 1.0
+
+import pygmsh
+
+# Generate local mesh.
+with pygmsh.occ.Geometry() as geom:
+    geom.characteristic_length_max = 0.1
+    if dim == 2:
+        ndimspherefunc = geom.add_disk
+    else:
+        ndimspherefunc = geom.add_ball
+    ball_outer = ndimspherefunc(
+        [
+            0.0,
+        ]
+        * dim,
+        radius_outer,
+    )
+
+    if radius_inner > 0.0:
+        ball_inner = ndimspherefunc(
+            [
+                0.0,
+            ]
+            * dim,
+            radius_inner,
+        )
+        geom.boolean_difference(ball_outer, ball_inner)
+
+    pygmesh0 = geom.generate_mesh()
+
+
+
+
+
+
+
import meshio
+
+
+
+
+
+
+
mesh = uw.meshing.Annulus(radiusOuter=1.0, radiusInner=0.0, cellSize=0.1)
+
+
+
+
+
+
+
mesh.dm.view()
+
+
+
+
+
+
+
t_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=3)
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+
+if PETSc.Comm.size == 1:
+
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshbox)
+
+    clipped_stack = pvmesh.clip(
+        origin=(0.0, 0.0, 0.0), normal=(-1, -1, 0), invert=False
+    )
+
+    pl = pv.Plotter()
+
+    # pl.add_mesh(pvmesh,'Blue', 'wireframe' )
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        use_transparency=False,
+    )
+    pl.show()
+
+
+
+
+
+
+
# Create Poisson object
+
+poisson = uw.systems.Poisson(
+    mesh, u_Field=t_soln, solver_name="poisson", verbose=True
+)
+
+poisson.constitutive_model = uw.constitutive_models.DiffusionModel
+poisson.k = k
+poisson.f = 0.0
+
+bcs_var = uw.discretisation.MeshVariable("bcs", mesh, 1)
+
+
+
+
+
+
+
import sympy
+
+abs_r = sympy.sqrt(mesh.rvec.dot(mesh.rvec))
+bc = sympy.cos(2.0 * mesh.N.y)
+
+with mesh.access(bcs_var):
+    bcs_var.data[:, 0] = uw.function.evaluate(bc, bcs_var.coords)
+
+poisson.add_dirichlet_bc(bcs_var.sym[0], "Upper", components=0)
+poisson.add_dirichlet_bc(-1.0, "Centre", components=0)
+
+
+
+
+
+
+
poisson.petsc_options.getAll()
+
+
+
+
+
+
+
# poisson._setup_terms()
+
+
+
+
+
+
+
poisson.solve()
+
+
+
+
+
+
+
# check the mesh if in a notebook / serial
+
+if uw.mpi.size == 1:
+
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh)
+
+    pvmesh.point_data["T"] = uw.function.evaluate(t_soln.fn, mesh.data)
+
+    # clipped_stack = pvmesh.clip(origin=(0.001,0.0,0.0), normal=(1, 0, 0), invert=False)
+
+    pl = pv.Plotter()
+
+    # pl.add_mesh(pvmesh,'Blue', 'wireframe' )
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        use_transparency=False,
+    )
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
mesh.petsc_save_checkpoint(index=0, meshVars=[t_soln], outputPath='./output/')
+
+
+
+
+
+
+
## We should try the non linear version of this next ...
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Utilities/Ex_ConstitutiveTensors.html b/main/Notebooks/Examples-Utilities/Ex_ConstitutiveTensors.html new file mode 100644 index 0000000..256bb25 --- /dev/null +++ b/main/Notebooks/Examples-Utilities/Ex_ConstitutiveTensors.html @@ -0,0 +1,1947 @@ + + + + + + + + + + + Constitutive relationships in Underworld (pt 1) — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + +
+ +
+

Constitutive relationships in Underworld (pt 1)#

+

Introduction to how the constitutive relationships in Underworld are formulated.

+
+
+
import petsc4py
+from petsc4py import PETSc
+
+
+
+
+
+
+
# %%
+import sys
+
+
+
+
+
+
+
# %%
+import underworld3 as uw
+from underworld3.systems import Poisson
+import numpy as np
+import sympy
+
+
+
+
+
+
+
import pytest
+from IPython.display import display  # since pytest runs pure python
+
+
+
+
+
+
+
# %%
+mesh2 = uw.meshing.UnstructuredSimplexBox(minCoords=(0.0,0.0), 
+                                              maxCoords=(1.0,1.0), 
+                                              cellSize=1.0/48.0, 
+                                              regular=True)
+
+
+
+
+
+
+
u2 = uw.discretisation.MeshVariable(r"\mathbf{u2}", mesh2, mesh2.dim, vtype=uw.VarType.VECTOR, degree=2)
+p2 = uw.discretisation.MeshVariable(r"p^{(2)}", mesh2, mesh2.dim, vtype=uw.VarType.SCALAR, degree=1)
+phi2 = uw.discretisation.MeshVariable(r"\phi^{(2)}", mesh2, 1, vtype=uw.VarType.SCALAR, degree=2)
+
+
+
+
+
+
+
mesh3 = uw.meshing.UnstructuredSimplexBox(minCoords=(0.0,0.0,0.0), 
+                                              maxCoords=(1.0,1.0,1.0), 
+                                              cellSize=1.0/8.0, 
+                                              regular=True)
+
+
+
+
+
+
+
u3 = uw.discretisation.MeshVariable(r"\mathbf{u3}", mesh3, mesh3.dim, vtype=uw.VarType.VECTOR, degree=2)
+p3 = uw.discretisation.MeshVariable(r"p^{(3)}", mesh3, 1, vtype=uw.VarType.SCALAR, degree=1)
+phi3 = uw.discretisation.MeshVariable(r"\phi^{(3)}", mesh3, 1, vtype=uw.VarType.SCALAR, degree=2)
+
+
+
+
+
+
+
R  = sympy.Array(sympy.matrices.rot_axis3(sympy.pi)[0:2,0:2])
+R4  = sympy.Array(sympy.matrices.rot_axis3(sympy.pi/4)[0:2,0:2])
+R2 = sympy.Array(sympy.matrices.rot_axis3(sympy.pi/2)[0:2,0:2])
+R34 = sympy.Array(sympy.matrices.rot_axis3(3*sympy.pi/4)[0:2,0:2])
+
+
+
+
+
+
+
# %%
+poisson2 = uw.systems.Poisson(mesh2, u_Field=phi2)
+poisson3 = uw.systems.Poisson(mesh3, u_Field=phi3)
+
+
+
+
+
+
+
stokes2 = uw.systems.Stokes(mesh2, velocityField=u2, pressureField=p2)
+stokes3 = uw.systems.Stokes(mesh3, velocityField=u3, pressureField=p3)
+
+
+
+
+
+
+
# %%
+stokes2
+
+
+
+
+
+
+

Tests#

+

The following tests are implemented with pytest.

+
+

Introduction to constitutive models#

+

Constitutive models relate fluxes of a quantity to the gradients of the unknowns. For example, in thermal diffusion, there is a relationship between heat flux and temperature gradients which is known as Fourier’s law and which involves an empirically determined thermal conductivity.

+
+\[ q_i = k \frac{ \partial T}{\partial x_i} \]
+

and in elasticity or fluid flow, we have a relationship between stresses and strain (gradients of the displacements) or strain-rate (gradients of velocity) respectively

+
+\[ \tau_{ij} = \mu \left( \frac{\partial u_i}{\partial x_j} + \frac{\partial u_j}{\partial x_i} \right) \]
+

where \(\eta\), here, is a material property (elastic shear modulus, viscosity) that we need to determine experimentally.

+

These material properties can be non-linear (they depend upon the quantities that they relate), and they can also be anisotropic (they vary with the orientation of the material). In the latter case, the material property is described through a consitutive tensor. For example:

+
+\[ q_i = k_{ij} \frac{ \partial T}{\partial x_j} \]
+

or

+
+\[ \tau_{ij} = \mu_{ijkl} \left( \frac{\partial u_k}{\partial x_l} + \frac{\partial u_l}{\partial x_k} \right) \]
+
+
+

Constitutive tensors#

+

In Underworld, the underlying implementation of any consitutive relationship is through the appropriate tensor representation, allowing for a very general formulation for most problems. The constitutive tensors are described symbolically using sympy expressions. For scalar equations, the rank 2 constitutive tensor (\(k_{ij}\)) can be expressed as a matrix without any loss of generality.

+

For vector equations, the constitutive tensor is of rank 4, but it is common in the engineering / finite element literature to transform the rank 4 tensor to a matrix representation. This has some benefits in terms of legibility and, perhaps, results in common code patterns across the formulation for scalar equations.

+

We can write the stress and strain rate tensors (\(\tau_{ij}\) and \(\dot\epsilon_{ij}\) respectively) as

+
+\[ \tau_I = \left( \tau_{00}, \tau_{11}, \tau_{22}, \tau_{12}, \tau_{02}, \tau_{01} \right) \]
+
+\[ \dot\varepsilon_I = \left( \dot\varepsilon_{00}, \dot\varepsilon_{11}, \dot\varepsilon_{22}, \dot\varepsilon_{12}, \dot\varepsilon_{02}, \dot\varepsilon_{01} \right) \]
+

where \(I\) indexes \(\{i,j\}\) in a specified order and symmetry of the tensor implies only six (in 3D) unique entries in the nine entries of a general tensor. With these definitions, the constitutive tensor can be written as \(C_{IJ}\) where \(I\), and \(J\) range over the independent degrees of freedom in stress and strain (rate). This is a re-writing of the full tensor, with the necessary symmetry required to respect the symmetry of \(\tau\) and \(\dot\varepsilon\)

+

The simplified notation comes at a cost. For example, \(\tau_I \dot\varepsilon_I\) is not equivalent to the tensor inner produce \(\tau_{ij} \dot\varepsilon_{ij}\), and \(C_{IJ}\) does not transform correctly for a rotation of coordinates.

+

However, a modest transformation of the matrix equations is helpful:

+
+\[ \tau^*_{I} = C^*_{IJ} \varepsilon^*_{J} \]
+

Where

+
+\[\tau^*_{I} = P_{IJ} \tau^*_J\]
+
+\[\varepsilon^*_I = P_{IJ} \varepsilon^*_J\]
+
+\[C^*_{IJ} = P_{IK} C_{KL} P_{LJ}\]
+

\(\mathbf{P}\) is the scaling matrix

+
+\[\begin{split}P = \left[\begin{matrix}1 & 0 & 0 & 0 & 0 & 0\\0 & 1 & 0 & 0 & 0 & 0\\0 & 0 & 1 & 0 & 0 & 0\\0 & 0 & 0 & \sqrt{2} & 0 & 0\\0 & 0 & 0 & 0 & \sqrt{2} & 0\\0 & 0 & 0 & 0 & 0 & \sqrt{2}\end{matrix}\right]\end{split}\]
+

In this form, known as the Mandel notation, \(\tau^*_I\varepsilon^*_I \equiv \tau_{ij} \varepsilon_{ij}\), and the fourth order identity matrix:

+
+\[I_{ijkl} = \frac{1}{2} \left( \delta_{ij}\delta_{kj} + \delta_{kl}\delta_{il} \right)\]
+

transforms to

+
+\[I_{IJ} = \delta_{IJ}\]
+
+
+

Mandel form, sympy tensorial form, Voigt form#

+

The rank 4 tensor form of the constitutive equations, \(c_{ijkl}\), has the following representation in sympy:

+
+\[\begin{split}\left[\begin{matrix}\left[\begin{matrix}c_{0 0 0 0} & c_{0 0 0 1}\\c_{0 0 1 0} & c_{0 0 1 1}\end{matrix}\right] & \left[\begin{matrix}c_{0 1 0 0} & c_{0 1 0 1}\\c_{0 1 1 0} & c_{0 1 1 1}\end{matrix}\right]\\\left[\begin{matrix}c_{1 0 0 0} & c_{1 0 0 1}\\c_{1 0 1 0} & c_{1 0 1 1}\end{matrix}\right] & \left[\begin{matrix}c_{1 1 0 0} & c_{1 1 0 1}\\c_{1 1 1 0} & c_{1 1 1 1}\end{matrix}\right]\end{matrix}\right]\end{split}\]
+

and the inner product \(\tau_{ij} = c_{ijkl} \varepsilon_{kl} \) is written

+
tau = sympy.tensorcontraction(
+            sympy.tensorcontraction(
+                   sympy.tensorproduct(c, epsilon),(3,5)),(2,3))
+
+
+

However, the sympy.Matrix module allows a much simpler expression

+
tau_star = C_star * epsilon_star
+tau = P.inv() * tau_star
+
+
+

which we adopt in underworld for the display and manipulation of constitutive tensors.

+
+

Voigt form#

+

Computation of the stress tensor using \(\tau^*_I = C^*_{IJ}\varepsilon^*_J\) is equivalent to the following

+
+\[ \mathbf{P} \mathbf{\tau} = \mathbf{P} \mathbf{C} \mathbf{P} \cdot \mathbf{P} \mathbf{\varepsilon} \]
+

multiply by \(\mathbf{P}^{-1} \) and collecting terms:

+
+\[ \mathbf{\tau} = \mathbf{C} \mathbf{P}^2 \mathbf{\varepsilon} \]
+

This is the Voigt form of the constitutive equations and is generally what you will find in a finite element textbook. The Voigt form of the constitutive matrix is the rearrangement of the rank 4 constitutive tensor (with no scaling), the strain rate vector is usually combined with \(\mathbf{P}^2\), and the stress vector is raw. For example:

+
+\[\begin{split} \left[\begin{matrix}\tau_{0 0}\\\tau_{1 1}\\\tau_{0 1}\end{matrix}\right] = + \left[\begin{matrix}\eta & 0 & 0\\0 & \eta & 0\\0 & 0 & \frac{\eta}{2}\end{matrix}\right] \left[\begin{matrix}\dot\varepsilon_{0 0}\\\dot\varepsilon_{1 1}\\2 \dot\varepsilon_{0 1}\end{matrix}\right] +\end{split}\]
+

A full discussion of this can be found in Brannon (2018) and Helnwein (2001).

+
+
+

References.#

+

Brannon, R. (2018). Rotation Reflection and Frame Changes Orthogonal tensors in computational engineering mechanics. IOP Publishing. https://doi.org/10.1088/978-0-7503-1454-1

+

Helnwein, P. (2001). Some remarks on the compressed matrix representation of symmetric second-order and fourth-order tensors. Computer Methods in Applied Mechanics and Engineering, 190(22–23), 2753–2770. https://doi.org/10.1016/S0045-7825(00)00263-2

+
+
+
# %%
+epsdot = uw.maths.tensor.rank2_symmetric_sym("\\dot\\varepsilon", 2)
+display(epsdot)
+epsdot_vec = uw.maths.tensor.rank2_to_voigt(epsdot, 2)
+display(epsdot_vec)
+Pm = uw.maths.tensor.P_mandel[2]
+
+
+
+
+
+
+
# %%
+I4 = uw.maths.tensor.rank4_identity(2)
+I4v = uw.maths.tensor.rank4_to_voigt(I4,2)
+I4m = uw.maths.tensor.rank4_to_mandel(I4,2)
+
+
+
+
+
+
+
display(I4v)
+display(I4m)
+
+
+
+
+
+
+
+
+

What does this show then ?#

+
+
+
display(Pm * Pm * epsdot_vec.T)
+display(Pm * I4v * Pm * epsdot_vec.T)
+
+
+
+
+

This is the generic constitutive tensor with relevant symmetries for fluid mechanics problems

+
+
+
c4sym = uw.maths.tensor.rank4_symmetric_sym('c', 2)
+display(c4sym)
+
+
+
+
+
+
+

This is the mandel form of the constitutive matrix for constant viscosity#

+
+
+
eta = sympy.symbols("\eta")
+Ceta = sympy.Matrix.diag([eta]*3)
+display(Ceta)
+
+
+
+
+
+
+
# And this is the equivalent tensor
+display(uw.maths.tensor.mandel_to_rank4(Ceta, 2))
+
+
+
+
+
+
+
# %%
+Cv = uw.maths.tensor.rank4_to_voigt(c4sym,2)
+Cm = uw.maths.tensor.rank4_to_mandel(c4sym,2)
+
+
+
+
+
+
+
display(Cv)
+display(Cm)
+
+
+
+
+
+
+
# %%
+d=3
+display(uw.maths.tensor.rank4_to_voigt(uw.maths.tensor.rank4_identity(d), d))
+sympy.Array(sympy.symarray('C',(d,d,d,d)))
+
+
+
+
+
+
+

This is how we use those things#

+
+
+
ViscousFlow = uw.constitutive_models.TransverseIsotropicFlowModel
+ViscousFlow.Parameters.eta_0=sympy.symbols(r"\eta_0")
+ViscousFlow.Parameters.eta_1=sympy.symbols(r"\eta_1")
+ViscousFlow.Parameters.director=sympy.Matrix([1,0,0]).T
+
+
+
+
+
+
+
ViscousFlow.flux
+
+
+
+
+
+
+
stokes3.constitutive_model = ViscousFlow
+display(stokes3.strainrate)
+display(stokes3.constitutive_model.flux)
+
+
+
+
+
+
+
# %%
+ViscousFlow.Parameters.eta_0=sympy.symbols(r"\eta_0")
+ViscousFlow.Parameters.eta_1=sympy.symbols(r"\eta_0")
+ViscousFlow.Parameters.director=sympy.Matrix([1,0,0]) # Doesn't matter if the viscosity are the same
+
+
+
+
+
+
+
stokes3.constitutive_model.flux
+
+
+
+
+
+
+
# %%
+Cmods = uw.constitutive_models.TransverseIsotropicFlowModel(u3)
+Cmods.Parameters.eta_0=sympy.symbols(r"\eta_0")
+Cmods.Parameters.eta_1=sympy.symbols(r"\eta_1")
+
+
+
+
+
+
+
Cmods.Parameters.director=sympy.Matrix([1,0,0]).T
+display(Cmods.C)
+
+
+
+
+
+
+
Cmods.Parameters.director=sympy.Matrix([0,1,0]).T
+display(Cmods.C)
+
+
+
+
+
+
+
# %%
+# Description / help is especially useful in notebook form
+Cmods.view()
+
+
+
+
+
+
+
# %%
+gradT = mesh2.vector.gradient(phi2.sym)
+Cmodp = uw.constitutive_models.Constitutive_Model(mesh2.dim, 1)
+Cmodp
+
+
+
+
+
+
+
# %%
+Cmodp.Parameters.k = sympy.symbols("\\kappa")
+display(Cmodp.C)
+display(Cmodp.c)
+
+
+
+
+
+
+
# %%
+Cmodp.flux(gradT)
+
+
+
+
+
+
+
# %%
+Cmods.k = sympy.symbols("\\eta")
+Cmods.c
+
+
+
+
+
+
+
# %%
+Cmods
+
+
+
+
+
+
+
# %%
+Cmodv = uw.constitutive_models.ViscousFlowModel(2)
+Cmodv.Parameters.viscosity = sympy.symbols(r"\eta")
+Cmodv
+
+
+
+
+
+
+
# %%
+Cmodv.flux(epsdot)
+
+
+
+
+

Cvisc = sympy.symbols(r’\eta’) * uw.maths.tensor.rank4_identity(2) +Celas = sympy.symbols(r’\mu’) * uw.maths.tensor.rank4_identity(2)

+

Cvisc + Celas

+
+
+

Equivalence test: define tensor explicitly or in canonical form with rotation#

+
+
+
# Rotation (as defined for Muhlhaus / Moresi transverse isotropy)
+
+n = sympy.Matrix(sympy.symarray("n",(3,)))
+s = sympy.Matrix((-n[1], n[0], 0))
+t = -mesh3.vector.cross(n,s).T # complete the coordinate triad
+
+
+
+
+
+
+
R = sympy.BlockMatrix((s,n,t)).as_explicit()
+R
+
+
+
+
+
+
+

Equivalence test - 2D tensor rotation#

+
+
+
Delta = sympy.symbols(r"\Delta")
+lambda_matrix = sympy.Matrix.diag([0,0, Delta])
+lambda_ani = uw.maths.tensor.mandel_to_rank4(lambda_matrix,2)
+lambda_xyz = sympy.simplify(uw.maths.tensor.tensor_rotation(R2, lambda_ani))
+lambda_xyz_m = sympy.simplify(uw.maths.tensor.rank4_to_mandel(lambda_xyz, 2))
+
+
+
+
+
+
+
lambda_matrix
+
+
+
+
+
+
+

Equivalence test - 2D tensor definition#

+
+
+
d=2
+lambda_mat2 = uw.maths.tensor.rank4_identity(2) * 0
+lambda_mat2
+
+
+
+
+
+
+
for i in range(d):
+    for j in range(d):
+        for k in range(d):
+            for l in range(d):
+                lambda_mat2[i,j,k,l] = Delta * ((n[i]*n[k]*int(j==l) + n[j]*n[k] * int(l==i) + 
+                                                 n[i]*n[l]*int(j==k) + n[j]*n[l] * int(k==i))/2 
+                                                 - 2 * n[i]*n[j]*n[k]*n[l] )
+
+
+
+
+
+
+
lambda_mat_m = sympy.simplify(uw.maths.tensor.rank4_to_mandel(lambda_mat2,2))
+difference = sympy.simplify(lambda_mat_m - lambda_xyz_m)
+
+
+
+
+
+
+
difference
+
+
+
+
+
+
+
# Are they term-by-term equivalent 
+sympy.simplify(difference.subs(n[1],sympy.sqrt(1-n[0]**2)))
+
+
+
+
+
+
+
n = sympy.Matrix(sympy.symarray("n",(3,)))
+s = (sympy.Matrix((-n[1], n[0], 0)) + sympy.Matrix((0, -n[2], n[1])))/2 
+s /= sympy.sqrt((s.dot(s)))
+
+
+
+
+
+
+
n_ijk = mesh3.vector.to_vector(n.T)
+s_ijk = mesh3.vector.to_vector(s.T)
+t = mesh3.vector.to_matrix(n_ijk.cross(s_ijk)).T
+
+
+
+
+
+
+
R = sympy.BlockMatrix((n,s,t)).as_explicit()
+R
+
+
+
+
+
+
+

Equivalence test - 3D tensor rotation#

+
+
+
Delta = sympy.symbols(r"\Delta")
+lambda_matrix = sympy.Matrix.diag([0, 0, 0, 0, Delta , Delta])
+lambda_ani = uw.maths.tensor.mandel_to_rank4(lambda_matrix,3)
+lambda_xyz = sympy.simplify(uw.maths.tensor.tensor_rotation(R, lambda_ani))
+lambda_xyz_m = sympy.simplify(uw.maths.tensor.rank4_to_mandel(lambda_xyz, 3))
+
+
+
+
+
+
+
lambda_matrix
+
+
+
+
+
+
+

Equivalence test - 3D tensor definition#

+
+
+
d=3
+lambda_mat2 = uw.maths.tensor.rank4_identity(d) * 0
+
+
+
+
+
+
+
for i in range(d):
+    for j in range(d):
+        for k in range(d):
+            for l in range(d):
+                lambda_mat2[i,j,k,l] = Delta * ((n[i]*n[k]*int(j==l) + n[j]*n[k] * int(l==i) + 
+                                                 n[i]*n[l]*int(j==k) + n[j]*n[l] * int(k==i))/2 
+                                                 - 2 * n[i]*n[j]*n[k]*n[l] )
+
+
+
+
+
+
+
lambda_mat_m = sympy.simplify(uw.maths.tensor.rank4_to_mandel(lambda_mat2,d))
+difference = sympy.simplify(lambda_mat_m - lambda_xyz_m)
+
+
+
+
+
+
+
# Are they term-by-term equivalent (should be if n is a unit vector)
+sympy.simplify(difference.subs(n[2],sympy.sqrt(1-n[0]**2-n[1]**2)))
+
+
+
+
+
+
+
# Muhlhaus et al 
+
+eta0 = sympy.symbols("\eta_0")
+eta1 = sympy.symbols("\eta_1")
+
+lambda_MM = uw.maths.tensor.rank4_identity(3) * 0
+lambda_MM_mandel = uw.maths.tensor.rank4_to_mandel(lambda_MM, 3)
+lambda_MM_mandel[0,0] = eta0
+lambda_MM_mandel[1,1] = eta0
+lambda_MM_mandel[2,2] = eta0
+lambda_MM_mandel[3,3] = eta0
+lambda_MM_mandel[4,4] = eta1
+lambda_MM_mandel[5,5] = eta1
+lambda_MM = uw.maths.tensor.mandel_to_rank4(lambda_MM_mandel, 3)
+lambda_xyz = sympy.simplify(uw.maths.tensor.tensor_rotation(R, lambda_MM))
+lambda_xyz_m = sympy.simplify(uw.maths.tensor.rank4_to_mandel(lambda_xyz, 3))
+
+
+
+
+
+
+
lambda_xyz_m
+
+
+
+
+

Note: the two forms are equivalent, the second is simpler to implement and sympy probably likes it better.

+
+
+
# %%
+0/0
+
+
+
+
+
+
+

What does the identity tensor look like in C_ijkl, converted ?#

+
+
+
d = 2
+
+
+
+
+
+
+
I = sympy.MutableDenseNDimArray.zeros(d,d,d,d)
+
+
+
+
+
+
+
for i in range(d):
+    for j in range(d):
+        for k in range(d):
+            for l in range(d):
+                I[i,j,k,l] = eta * sympy.sympify((i==k)*(j==l) + (i==l)*(j==k)) / 2
+
+
+
+
+
+
+
I
+
+
+
+
+
+
+
uw.maths.tensor.rank4_to_mandel(I, 2)
+
+
+
+
+
+
+
uw.maths.tensor.rank4_to_voigt(I, 2)
+
+
+
+
+
+
+
# %%
+uw.maths.tensor.voigt_to_rank4(P.inv() * sympy.Matrix.eye(6) * P.inv(),2)
+
+
+
+
+
+
+
# %%
+E3 = sympy.Matrix(sympy.symarray('\dot\epsilon',(3,3)))
+E3[1,0] = E3[0,1] # enforce symmetry
+E3[2,0] = E3[0,2] #    --- " ---
+E3[2,1] = E3[1,2] #    --- " ---
+
+
+
+
+
+
+
mesh3.tensor.rank2_to_mandel(E3)
+
+
+
+
+
+
+
# %%
+tau_r = 2 * sympy.tensorcontraction(sympy.tensorcontraction(sympy.tensorproduct(I, E3),(3,5)),(2,3))
+
+
+
+
+
+
+
tau_r
+
+
+
+
+
+
+
# %%
+0/0
+
+
+
+
+
+
+
# %%
+P*mesh3.tensor.rank4_to_voigt(I)*P
+
+
+
+
+
+
+
# %%
+P * P * Cmd * Em
+
+
+
+
+
+
+
# %%
+P = sympy.Matrix.diag([1, 1, 1, sympy.sqrt(2), sympy.sqrt(2), sympy.sqrt(2)])
+eta = sympy.symbols("\eta")
+C = sympy.Matrix.diag([eta]*6)
+
+
+
+
+
+
+
# %%
+Pm = 
+
+
+
+
+
+
+
Cv
+
+
+
+
+
+
+
# %%
+P * Cmods.template_L # this is the strain rate in Mandel form
+
+
+
+
+
+
+
# %%
+0/0
+
+
+
+
+
+
+
# %%
+0/0
+
+
+
+
+
+
+
# %%
+sympy.Matrix(sympy.symarray('\partial\phi,',(d,)))
+
+
+
+
+
+
+
# %%
+M = sympy.Matrix(sympy.symarray("\\left(\\nabla\\mathbf{u}\\right)",(3,)))
+M
+
+
+
+
+
+
+
# %%
+Mt = sympy.Matrix(sympy.symarray("\\left(\\nabla\\mathbf{u^*} + \\nabla\\mathbf{u^*}^T\\right)",(3,)))
+Mt
+
+
+
+
+
+
+
# %%
+C.is_symmetric()
+
+
+
+
+
+
+
# %%
+isinstance(stokes, uw.systems.SNES_Vector)
+
+
+
+
+
+
+
# %%
+# Set some things
+poisson.k = 1. 
+poisson.f = 0.
+poisson.add_dirichlet_bc( 1., "Bottom" )  
+poisson.add_dirichlet_bc( 0., "Top" )  
+
+
+
+
+
+
+
# %%
+poisson._setup_terms()
+poisson.snes.setTolerances(rtol=1.0e-6)
+poisson.snes.ksp.monitorCancel()
+
+
+
+
+
+
+
# %%
+# Solve time
+poisson.solve()
+
+
+
+
+
+
+
# %%
+display(poisson._f1)
+display(poisson._L)
+
+
+
+
+
+
+
# %%
+Cmod = uw.constitutive_models.Constitutive_Model(poisson)
+
+
+
+
+
+
+
# %%
+Cmod.template_c
+
+
+
+
+
+
+
# %%
+Cmod3 = uw.constitutive_models.Constitutive_Model(poisson3)
+Cmod3.template_c
+
+
+
+
+
+
+
# %%
+Cmods = uw.constitutive_models.Constitutive_Model(stokes)
+
+
+
+
+
+
+
# %%
+Cmods.template_c
+
+
+
+
+
+
+
# %%
+mesh.tensor.rank4_to_voigt(Cmods.template_c)
+
+
+
+
+
+
+
# %%
+import sympy
+K = sympy.Matrix.eye(2,2)
+K[0,0] = 100
+
+
+
+
+
+
+
# %%
+R = sympy.matrices.rot_axis3(sympy.pi)[0:2,0:2]
+
+
+
+
+
+
+
# %%
+K
+
+
+
+
+
+
+
# %%
+K1 = R.T * K * R
+
+
+
+
+
+
+
# %%
+(K1 * poisson._L.T).T
+
+
+
+
+

https://farside.ph.utexas.edu/teaching/336L/Fluidhtml/node250.html

+

Would be useful to demonstrate how to do tensorproduct / tensor contraction rotation of a tensor +to make sure it works for rank 4 case.

+

Define \({\cal R}_{ij}\) as the rotation matrix that maps \(x\) onto the \(x' \) coordiate system, i.e.

+

\(\displaystyle a_i' = {\cal R}_{ij}\,a_j,\), for vectors,

+

which also has this property:

+

\(\displaystyle {\cal R}_{ki}\,{\cal R}_{kj } = \delta_{ij}.\)

+

Second order tensors transform as follows:

+

\(\displaystyle a_{ij}' = {\cal R}_{ik}\,{\cal R}_{jl}\,a_{kl},\)

+

and for higher rank tensors, we just continue …

+

\(\displaystyle a_{ijk}' = {\cal R}_{il}\,{\cal R}_{jm}\,{\cal R}_{kn}\,a_{lmn}.\)

+

2D example - this should be one of the tests.

+
+
+
R  = sympy.Array(sympy.matrices.rot_axis3(sympy.pi)[0:2,0:2])
+R4  = sympy.Array(sympy.matrices.rot_axis3(sympy.pi/4)[0:2,0:2])
+R2 = sympy.Array(sympy.matrices.rot_axis3(sympy.pi/2)[0:2,0:2])
+R34 = sympy.Array(sympy.matrices.rot_axis3(3*sympy.pi/4)[0:2,0:2])
+
+
+
+
+
+
+
display(R)
+display(R2)
+display(R4)
+display(R34)
+
+
+
+
+
+
+
# %%
+def tensor_rotation(R, T):
+    rank = T.rank()
+    print("Rank = ",rank)
+    
+    # Tc = T.copy()
+    
+    for i in range(rank):
+        T = sympy.tensorcontraction(sympy.tensorproduct(R,T),(1,rank+1))
+    
+    return T
+
+
+
+
+
+
+
A = sympy.Array(sympy.symarray('a',(2,2)))
+C = sympy.Array(sympy.symarray('c',(2,2,2,2)))
+V = sympy.Array(sympy.symarray('v',(2,)))
+
+
+
+
+
+
+
A3 = sympy.Array(sympy.symarray('a',(3,3)))
+C3 = sympy.Array(sympy.symarray('c',(3,3,3,3)))
+V3 = sympy.Array(sympy.symarray('v',(3,)))
+
+
+
+
+
+
+
# %%
+CC = sympy.MutableDenseNDimArray(0 * C)
+eta0 = sympy.symbols("\eta_0")
+eta1 = sympy.symbols("\eta_1")
+
+
+
+
+
+
+
CC[0,0,0,0] = CC[1,1,1,1] = 2 * eta0
+CC[0,1,0,1] = CC[1,0,1,0] = eta1
+CC[0,1,1,0] = CC[1,0,0,1] = eta1
+
+
+
+
+
+
+
E = sympy.MutableDenseNDimArray(sympy.symarray('\dot\epsilon',(2,2)))
+E[0,1] = E[1,0] # enforce symmetry
+
+
+
+
+
+
+
CC3 = sympy.MutableDenseNDimArray(0 * C3)
+eta30 = sympy.symbols("\eta_0")
+eta31 = sympy.symbols("\eta_1")
+
+
+
+
+
+
+
CC3[0,0,0,0] = CC3[1,1,1,1] = CC3[2,2,2,2] = 2 * eta0
+
+
+
+
+
+
+
CC3[0,1,0,1] = CC3[1,0,1,0] = eta1
+CC3[0,1,1,0] = CC3[1,0,0,1] = eta1
+
+
+
+
+
+
+
CC3[1,2,1,2] = CC3[2,1,2,1] = eta1
+CC3[2,1,1,2] = CC3[1,2,2,1] = eta1
+
+
+
+
+
+
+
CC3[0,2,0,2] = CC3[2,0,2,0] = eta0
+CC3[2,0,0,2] = CC3[0,2,2,0] = eta0
+
+
+
+
+
+
+
E3 = sympy.MutableDenseNDimArray(sympy.symarray('\dot\epsilon',(3,3)))
+E3[1,0] = E3[0,1] # enforce symmetry
+E3[2,0] = E3[0,2] #    --- " ---
+E3[2,1] = E3[1,2] #    --- " ---
+
+
+
+
+
+
+
# %%
+P = sympy.Matrix.zeros(6,6)
+P[0,0] = P[1,1] = P[2,2] = 1
+P[3,3] = P[4,4] = P[5,5] = 2
+
+
+
+
+
+
+
# %%
+tau_v = Cv * P * Ev.T
+tau_v
+
+
+
+
+
+
+
# %%
+display(voigt_to_rank2(tau_v))
+mesh3.tensor.voigt_to_rank2(tau_v)
+
+
+
+
+
+
+
# %%
+tau = sympy.tensorcontraction(sympy.tensorcontraction(sympy.tensorproduct(CC3, E3),(3,5)),(2,3))
+display(tau)
+
+
+
+
+
+
+
# %%
+CCr = tensor_rotation(R34, CC)
+tau_r = sympy.tensorcontraction(sympy.tensorcontraction(sympy.tensorproduct(CCr, E),(3,5)),(2,3))
+tau_r
+
+
+
+
+
+
+
display(sympy.simplify(CCr))
+display(sympy.simplify(tau_r))
+
+
+
+
+
+
+
# %%
+display(tensor_rotation(R2, A))
+display(tensor_rotation(R2, V))
+
+
+
+
+
+
+
A2 = tensor_rotation(R2, A)
+A3 = tensor_rotation(R2, A2)
+
+
+
+
+
+
+
# %%
+Crot = tensor_rotation(R2, tensor_rotation(R2, C))
+
+
+
+
+
+
+
# %%
+Crot - C
+
+
+
+
+
+
+
# %%
+C = sympy.tensorcontraction(sympy.tensorproduct(R,C),(1,3))
+C.shape
+C
+
+
+
+
+
+
+
# %%
+sympy.tensorcontraction(sympy.tensorproduct(R,R),(1,2))
+
+
+
+
+
+
+
# %%
+sympy.tensorproduct(R,R).shape
+
+
+
+
+
+
+
# %%
+R.transpose()
+
+
+
+
+
+
+
# %%
+0/0
+
+
+
+
+

Check. Construct simple linear which is solution for +above config. Exclude boundaries from mesh data.

+
+
+
import numpy as np
+with mesh.access():
+    mesh_numerical_soln = uw.function.evaluate(poisson.u.fn, mesh.data)
+    mesh_analytic_soln = uw.function.evaluate(1.0-mesh.N.y, mesh.data)
+    if not np.allclose(mesh_analytic_soln, mesh_numerical_soln, rtol=0.01):
+        raise RuntimeError("Unexpected values encountered.")
+
+
+
+
+
+
+
# %%
+(mesh_analytic_soln - mesh_numerical_soln).max()
+
+
+
+
+

Validate

+
+
+
from mpi4py import MPI
+
+
+
+
+
+
+
if MPI.COMM_WORLD.size==1:
+
+    import numpy as np
+    import pyvista as pv
+    import vtk
+
+    pv.global_theme.background = 'white'
+    pv.global_theme.window_size = [500, 500]
+    pv.global_theme.antialiasing = True
+    pv.global_theme.jupyter_backend = 'panel'
+    pv.global_theme.smooth_shading = True
+    
+    mesh.vtk("ignore_box.vtk")
+    pvmesh = pv.read("ignore_box.vtk")
+        
+
+
+    with mesh.access():
+        pvmesh.point_data["T"]  = mesh_analytic_soln
+        pvmesh.point_data["T2"] = mesh_numerical_soln
+        pvmesh.point_data["DT"] = pvmesh.point_data["T"] - pvmesh.point_data["T2"] 
+    
+    pl = pv.Plotter()
+
+    pl.add_mesh(pvmesh, cmap="coolwarm", edge_color="Black", show_edges=True, scalars="DT",
+                  use_transparency=False, opacity=0.5)
+         
+    pl.show(cpos="xy")
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Utilities/Ex_Integrals_on_Meshes.html b/main/Notebooks/Examples-Utilities/Ex_Integrals_on_Meshes.html new file mode 100644 index 0000000..957e2ac --- /dev/null +++ b/main/Notebooks/Examples-Utilities/Ex_Integrals_on_Meshes.html @@ -0,0 +1,803 @@ + + + + + + + + + + + Using the PETSc mesh integration routines — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Using the PETSc mesh integration routines

+ +
+
+ +
+
+
+ + + + +
+ +
+

Using the PETSc mesh integration routines#

+

This is probably better moved to become a test !

+
+
+
import underworld3 as uw
+import numpy as np
+import sympy
+
+
+
+
+
+
+
mesh = uw.meshing.UnstructuredSimplexBox(minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0), cellSize=1.0 / 32.0, regular=True)
+s_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=2) # add this line to avoid petsc error for time being
+
+x = mesh.N.x
+y = mesh.N.y
+z = mesh.N.z
+
+I = uw.maths.Integral(mesh, x * y)
+print(I.evaluate())  # 0.25
+
+
+
+
+
+
+
mesh = uw.meshing.StructuredQuadBox(minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0), elementRes=(32, 32))
+s_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=2) # add this line to avoid petsc error for time being
+
+x = mesh.N.x
+y = mesh.N.y
+z = mesh.N.z
+
+I2 = uw.maths.Integral(mesh, x * y)
+print(I2.evaluate())  # 0.25
+
+
+
+
+
+
+
mesh = uw.meshing.StructuredQuadBox(minCoords=(0.0, 0.0, 0.0), maxCoords=(1.0, 1.0, 1.0), elementRes=(8, 8, 8))
+s_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=2) # add this line to avoid petsc error for time being
+
+x = mesh.N.x
+y = mesh.N.y
+z = mesh.N.z
+
+I3 = uw.maths.Integral(mesh, x * y * z)
+print(I3.evaluate())  # 0.125
+
+
+
+
+
+
+
mesh = uw.meshing.UnstructuredSimplexBox(
+    minCoords=(0.0, 0.0, 0.0), maxCoords=(1.0, 1.0, 1.0), cellSize=1.0 / 8.0, regular=True
+)
+s_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=2) # add this line to avoid petsc error for time being
+
+x = mesh.N.x
+y = mesh.N.y
+z = mesh.N.z
+
+I4 = uw.maths.Integral(mesh, x * y * z)
+print(I4.evaluate())  # 0.125
+
+
+
+
+
+
+
mesh = uw.meshing.Annulus(radiusInner=0.5, radiusOuter=1.0, cellSize=0.05)
+s_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=2) # add this line to avoid petsc error for time being
+
+x = mesh.N.x
+y = mesh.N.y
+z = mesh.N.z
+
+I5 = uw.maths.Integral(mesh, 1.0)
+print(I5.evaluate())  # 3 * pi / 4 = 2.35
+
+
+
+
+
+
+
mesh = uw.meshing.Annulus(radiusInner=0.0, radiusOuter=1.0, cellSize=0.05)
+s_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=2) # add this line to avoid petsc error for time being
+
+x = mesh.N.x
+y = mesh.N.y
+z = mesh.N.z
+
+I6 = uw.maths.Integral(mesh, 1.0)
+print(I6.evaluate())  # pi
+
+
+
+
+
+
+
mesh = uw.meshing.SphericalShell(radiusInner=0.5, radiusOuter=1.0, cellSize=0.2)
+s_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=2) # add this line to avoid petsc error for time being
+
+x = mesh.N.x
+y = mesh.N.y
+z = mesh.N.z
+
+I7 = uw.maths.Integral(mesh, 1)
+print(I7.evaluate())  # 4/3 * 7/8 * pi
+
+
+
+
+
+
+
mesh = uw.meshing.SphericalShell(radiusInner=0.0, radiusOuter=1.0, cellSize=0.2)
+s_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=2) # add this line to avoid petsc error for time being
+
+x = mesh.N.x
+y = mesh.N.y
+z = mesh.N.z
+
+I8 = uw.maths.Integral(mesh, 1)
+print(I8.evaluate())  # 4/3 * pi
+
+
+
+
+
+
+
# mesh = uw.meshing.CubicSphere(radiusInner=0.5, radiusOuter=1.0, numElements=30)
+# s_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=2) # add this line to avoid petsc error for time being
+
+# x = mesh.N.x
+# y = mesh.N.y
+# z = mesh.N.z
+
+# I9 = uw.maths.Integral(mesh, 1)
+# print(I9.evaluate())  # 4/3 * 7/8 * pi (3.634)
+
+
+
+
+
+
+
mesh = uw.meshing.SphericalShell(radiusInner=0.0, radiusOuter=1.0, cellSize=0.5)
+# mesh = uw.meshing.Annulus(radiusInner = 0.0, radiusOuter=1.0, cellSize=0.05)
+s_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=2) # add this line to avoid petsc error for time being
+
+x = mesh.N.x
+y = mesh.N.y
+z = mesh.N.z
+
+radius_fn = sympy.sqrt(mesh.rvec.dot(mesh.rvec))  # normalise by outer radius if not 1.0
+unit_rvec = mesh.rvec / (1.0e-10 + radius_fn)
+
+r = sympy.sqrt(x**2 + y**2)
+th = sympy.atan2(y + 1.0e-10, x + 1.0e-10)
+
+v_soln = uw.discretisation.MeshVariable("U", mesh, mesh.dim, degree=2)
+p_soln = uw.discretisation.MeshVariable("P", mesh, 1, degree=1)
+t_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=3)
+
+swarm = uw.swarm.Swarm(mesh=mesh)
+v_star = uw.swarm.SwarmVariable("Vs", swarm, mesh.dim, proxy_degree=2)
+remeshed = uw.swarm.SwarmVariable("Vw", swarm, 1, dtype="int", _proxy=False)
+X_0 = uw.swarm.SwarmVariable("X0", swarm, mesh.dim, _proxy=False)
+
+swarm.populate(fill_param=4)
+
+I10 = uw.maths.Integral(mesh, 1)
+print(I10.evaluate())
+
+stokes = uw.systems.Stokes(
+    mesh,
+    velocityField=v_soln,
+    pressureField=p_soln,
+    # u_degree=v_soln.degree,
+    # p_degree=p_soln.degree,
+    verbose=False,
+    solver_name="stokes",
+)
+
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+
+stokes.add_dirichlet_bc((0.0, 0.0, 0.0), "Upper", (0, 1, 2))
+
+stokes.viscosity = 1.0
+stokes.bodyforce = mesh.rvec * sympy.sin(th)
+
+
+stokes.solve()
+
+I10 = uw.maths.Integral(mesh, v_soln.fn.dot(v_soln.fn))
+print(I10.evaluate())
+
+
+
+
+
+
+
## Check this this sort of thing works OK
+
+mesh = uw.meshing.SphericalShell(radiusInner=0.0, radiusOuter=1.0, cellSize=0.1)
+
+x = mesh.N.x
+y = mesh.N.y
+z = mesh.N.z
+
+meshvar = uw.discretisation.MeshVariable("phi", mesh, 1, degree=3)
+
+I = uw.maths.Integral(mesh, 1)
+print(I.evaluate())  # 4/3 * pi
+
+I.fn = 0.75
+print(I.evaluate())  # pi
+
+I.fn = x
+print(I.evaluate())  # should be zero
+
+I.fn = x + y + z
+print(I.evaluate())  # should be zero
+
+with mesh.access(meshvar):
+    meshvar.data[:, 0] = uw.function.evaluate(x + y + z, meshvar.coords)
+
+I.fn = meshvar.fn
+print(I.evaluate())  # should be zero
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Utilities/Ex_Projection_Function_Eval.html b/main/Notebooks/Examples-Utilities/Ex_Projection_Function_Eval.html new file mode 100644 index 0000000..0c6281f --- /dev/null +++ b/main/Notebooks/Examples-Utilities/Ex_Projection_Function_Eval.html @@ -0,0 +1,765 @@ + + + + + + + + + + + Projection-based function evaluation — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Projection-based function evaluation

+ +
+
+ +
+
+
+ + + + +
+ +
+

Projection-based function evaluation#

+

Here we Use SNES solvers to project sympy / mesh variable functions and derivatives to nodes. Pointwise / symbolic functions cannot always be evaluated using uw.function.evaluate because they contain a mix of mesh variables, derivatives and symbols which may not be defined everywhere.

+

Our solution is to use a projection of the function to a continuous mesh variable with the SNES machinery performing all of the background operations to determine the values and the optimal fitting.

+

This approach also allows us to include boundary conditions, smoothing, and constraint terms (e.g. remove a null space) in cases (like piecewise continuous swarm variables) where this is difficult in the original form.

+

We’ll demonstrate this using a swarm variable (scalar / vector), but the same approach is useful for gradient recovery.

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import underworld3 as uw
+import numpy as np
+import sympy
+
+
+
+
+
+
+
from underworld3.meshing import UnstructuredSimplexBox
+
+
+
+
+
+
+
meshbox = UnstructuredSimplexBox(minCoords=(0.0, 0.0), 
+                                 maxCoords=(1.0, 1.0), 
+                                 cellSize=1.0 / 32.0,)
+
+
+
+
+
+
+
# import meshio
+# mesh2 = meshio.read(filename="../Examples-StokesFlow/tmp_ball.vtk")
+# meshio.write(filename="tmp_ball.msh", mesh=mesh2)
+
+
+
+
+
+
+
import sympy
+
+# Some useful coordinate stuff
+
+x, y = meshbox.X
+
+
+
+
+
+
+
s_soln = uw.discretisation.MeshVariable("T", meshbox, 1, degree=2)
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable("U", meshbox, meshbox.dim, degree=2)
+
+
+
+
+
+
+
s_fn = sympy.cos(5.0 * sympy.pi * x) * sympy.cos(5.0 * sympy.pi * y)
+v_fn = sympy.Matrix([sympy.cos(5.0 * sympy.pi * y) ** 2, -sympy.sin(5.0 * sympy.pi * x) ** 2])  # divergence free
+
+
+
+
+
+
+
meshbox.vector.divergence(v_fn)
+
+
+
+
+
+
+
swarm = uw.swarm.Swarm(mesh=meshbox)
+s_values = uw.swarm.SwarmVariable("Ss", swarm, 1, proxy_degree=3)
+v_values = uw.swarm.SwarmVariable("Vs", swarm, meshbox.dim, proxy_degree=3)
+# iv_values = uw.swarm.SwarmVariable("Vi", swarm, meshbox.dim, proxy_degree=3)
+
+swarm.populate(fill_param=3)
+
+
+
+
+
+
+
scalar_projection = uw.systems.Projection(meshbox, s_soln)
+scalar_projection.uw_function = s_values.sym
+scalar_projection.smoothing = 1.0e-6
+
+
+
+
+
+
+
vector_projection = uw.systems.Vector_Projection(meshbox, v_soln)
+vector_projection.uw_function = v_values.sym
+vector_projection.smoothing = 1.0e-6  # see how well it works !
+vector_projection.penalty = 1.0e-6
+
+# Velocity boundary conditions (compare left / right walls in the soln !)
+
+vector_projection.add_dirichlet_bc(v_fn, "Left", (0, 1))
+vector_projection.add_dirichlet_bc(v_fn, "Right", (0, 1))
+vector_projection.add_dirichlet_bc(v_fn, "Top", (0, 1))
+vector_projection.add_dirichlet_bc(v_fn, "Bottom", (0, 1))
+
+
+
+
+
+
+
with swarm.access(s_values, v_values):
+    s_values.data[:, 0] = uw.function.evaluate(s_fn, swarm.data, meshbox.N)
+    v_values.data[:, 0] = uw.function.evaluate(v_fn[0], swarm.data, meshbox.N)
+    v_values.data[:, 1] = uw.function.evaluate(v_fn[1], swarm.data, meshbox.N)
+
+
+
+
+
+
+
scalar_projection.solve()
+
+
+
+
+
+
+
vector_projection.solve()
+
+
+
+
+
+
+
scalar_projection.uw_function = meshbox.vector.divergence(v_soln.sym)
+scalar_projection.solve()
+
+
+
+
+
+
+
s_soln.stats()
+
+
+
+
+
+
+
# check the projection
+
+
+if uw.mpi.size == 1:
+
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+    pvmesh = vis.mesh_to_pv_mesh(meshbox)
+    pvmesh.point_data["S"] = vis.scalar_fn_to_pv_points(pvmesh, s_soln.sym)
+
+    velocity_points = vis.meshVariable_to_pv_cloud(v_soln)
+    velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym)
+
+    pl = pv.Plotter(window_size=(1000, 750))
+
+    # pl.add_mesh(pvmesh,'Black', 'wireframe')
+
+    pl.add_mesh(
+        pvmesh, cmap="coolwarm", edge_color="Black", show_edges=True, scalars="S", use_transparency=False, opacity=0.5
+    )
+
+    pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=1.0e-1, opacity=0.5)
+    # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1)
+
+    # pl.add_points(pdata)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Utilities/Ex_Stokes_Annulus_Benchmark_Kramer_etal.html b/main/Notebooks/Examples-Utilities/Ex_Stokes_Annulus_Benchmark_Kramer_etal.html new file mode 100644 index 0000000..962ee70 --- /dev/null +++ b/main/Notebooks/Examples-Utilities/Ex_Stokes_Annulus_Benchmark_Kramer_etal.html @@ -0,0 +1,1042 @@ + + + + + + + + + + + Annulus Benchmark: Isoviscous Incompressible Stokes — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Annulus Benchmark: Isoviscous Incompressible Stokes

+ +
+ +
+
+ + + + +
+ +
+

Annulus Benchmark: Isoviscous Incompressible Stokes#

+
+

Case: Infinitely thin density anomaly at \(r = r'\)#

+
+

Benchmark paper#

+

Author: Thyagarajulu Gollapalli

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import underworld3 as uw
+from underworld3.systems import Stokes
+
+import numpy as np
+import sympy
+from sympy import lambdify
+import os
+import matplotlib.pyplot as plt
+import cmcrameri.cm as cmc
+import assess
+
+
+
+
+
+
+
os.environ["SYMPY_USE_CACHE"] = "no"
+
+
+
+
+
+
+
if uw.mpi.size == 1:
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+
+
+
+
+
+
# mesh options
+res = 1/8
+res_int_fac = 1/2
+r_o = 2.0
+r_int = 1.8
+r_i = 1.0
+
+
+
+
+
+
+
# visualize analytical solutions
+plot_ana = False
+
+n = 2 # wave number
+
+
+
+
+
+
+
+

Analytical Solution#

+
+
+
if plot_ana:
+    solution_above = assess.CylindricalStokesSolutionDeltaFreeSlip(n, +1, Rp=r_o, Rm=r_i, rp=r_int, nu=1.0, g=-1.0)
+    solution_below = assess.CylindricalStokesSolutionDeltaFreeSlip(n, -1, Rp=r_o, Rm=r_i, rp=r_int, nu=1.0, g=-1.0)
+
+
+
+
+
+
+
if plot_ana:
+    mesh_ana = uw.meshing.AnnulusInternalBoundary(radiusOuter=r_o, 
+                                                  radiusInternal=r_int, 
+                                                  radiusInner=r_i, 
+                                                  cellSize_Inner=res,
+                                                  cellSize_Internal=res*res_int_fac,
+                                                  cellSize_Outer=res,)
+
+
+
+
+
+
+
if plot_ana:
+    v_ana = uw.discretisation.MeshVariable(r"\mathbf{u_a}", mesh_ana, 2, degree=2)
+    p_ana = uw.discretisation.MeshVariable(r"p_a", mesh_ana, 1, degree=1)
+    rho_ana = uw.discretisation.MeshVariable(r"rho_a", mesh_ana, 1, degree=1)
+
+
+
+
+
+
+
if uw.mpi.size == 1 and plot_ana:
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh_ana)
+   
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_mesh(pvmesh, edge_color="Grey", show_edges=True, use_transparency=False, opacity=1.0, )
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
if plot_ana:
+    r_ana, th_ana = mesh_ana.CoordinateSystem.xR
+
+
+
+
+
+
+
if plot_ana:
+    with mesh_ana.access(v_ana, p_ana, rho_ana):
+        # velocities
+        r = uw.function.evalf(r_ana, v_ana.coords)
+        for i, coord in enumerate(v_ana.coords):
+            if r[i]>r_int:
+                v_ana.data[i] = solution_above.velocity_cartesian(coord)
+            else:
+                v_ana.data[i] = solution_below.velocity_cartesian(coord)
+        
+        
+        # pressure 
+        r = uw.function.evalf(r_ana, p_ana.coords)
+        for i, coord in enumerate(p_ana.coords):
+            if r[i]>r_int:
+                p_ana.data[i] = solution_above.pressure_cartesian(coord)
+            else:
+                p_ana.data[i] = solution_below.pressure_cartesian(coord)
+    
+        # density
+        r = uw.function.evalf(r_ana, rho_ana.coords)
+        for i, coord in enumerate(rho_ana.coords):
+            if r[i]>r_int:
+                rho_ana.data[i] = solution_above.radial_stress_cartesian(coord)
+            else:
+                rho_ana.data[i] = solution_below.radial_stress_cartesian(coord)
+
+
+
+
+
+
+
if uw.mpi.size == 1 and plot_ana:
+    pvmesh_ana = vis.mesh_to_pv_mesh(mesh_ana)
+    pvmesh_ana.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh_ana, v_ana.sym)
+    pvmesh_ana.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh_ana, 
+                                                               sympy.sqrt(v_ana.sym.dot(v_ana.sym)))
+    
+    print(pvmesh_ana.point_data["Vmag"].min(), pvmesh_ana.point_data["Vmag"].max())
+    
+    velocity_points_ana = vis.meshVariable_to_pv_cloud(v_ana)
+    velocity_points_ana.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points_ana, v_ana.sym)
+    
+    pl = pv.Plotter(window_size=(750, 750))
+    pl.add_mesh(pvmesh_ana, cmap=cmc.lapaz.resampled(11), edge_color="Grey",
+                scalars="Vmag", show_edges=False, use_transparency=False,
+                opacity=0.7, clim=[0., 0.05] )
+    pl.add_arrows(velocity_points_ana.points[::5], velocity_points_ana.point_data["V"][::5], 
+                  mag=5e0, color='k')
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
if uw.mpi.size == 1 and plot_ana:
+    pvmesh_ana = vis.mesh_to_pv_mesh(mesh_ana)
+    pvmesh_ana.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh_ana, p_ana.sym)
+
+    print(pvmesh_ana.point_data["P"].min(), pvmesh_ana.point_data["P"].max())
+   
+    pl = pv.Plotter(window_size=(750, 750))
+    pl.add_mesh(pvmesh_ana, cmap=cmc.vik.resampled(41), edge_color="Grey",
+                scalars="P", show_edges=False, use_transparency=False,
+                opacity=1.0, clim=[-0.65, 0.65] )
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
# This one is actually tau_rr
+
+if uw.mpi.size == 1 and plot_ana:
+    pvmesh_ana = vis.mesh_to_pv_mesh(mesh_ana)
+    pvmesh_ana.point_data["rho"] = vis.scalar_fn_to_pv_points(pvmesh_ana, rho_ana.sym)
+
+    print(pvmesh_ana.point_data["rho"].min(), pvmesh_ana.point_data["rho"].max())
+    
+    pl = pv.Plotter(window_size=(750, 750))
+    pl.add_mesh(pvmesh_ana, cmap=cmc.roma.resampled(41), edge_color="Grey",
+                scalars="rho", show_edges=False, use_transparency=False,
+                opacity=1.0) # , clim=[-0.8, 0.8] )
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+

Create Mesh for Numerical Solution#

+
+
+
# mesh
+mesh_uw = uw.meshing.AnnulusInternalBoundary(radiusOuter=r_o, 
+                                             radiusInternal=r_int, 
+                                             radiusInner=r_i, 
+                                             cellSize_Inner=res,
+                                             cellSize_Internal=res*res_int_fac,
+                                             cellSize_Outer=res,)
+
+
+
+
+
+
+
# mesh variables
+v_uw = uw.discretisation.MeshVariable(r"\mathbf{u}", mesh_uw, 2, degree=2)
+p_uw = uw.discretisation.MeshVariable(r"p", mesh_uw, 1, degree=1, continuous=True)
+v_err = uw.discretisation.MeshVariable(r"\mathbf{u_e}", mesh_uw, 2, degree=2)
+p_err = uw.discretisation.MeshVariable(r"p_e", mesh_uw, 1, degree=1, continuous=True)
+
+
+
+
+
+
+
# Some useful coordinate stuff
+unit_rvec = mesh_uw.CoordinateSystem.unit_e_0
+r_uw, th_uw = mesh_uw.CoordinateSystem.xR
+
+
+
+
+
+
+
# Create Stokes object
+
+stokes = Stokes(mesh_uw, velocityField=v_uw, pressureField=p_uw, solver_name="stokes")
+
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.viscosity = 1.0
+stokes.saddle_preconditioner = 1.0
+
+rho = sympy.cos(n*th_uw)  
+
+Gamma = unit_rvec 
+Gamma = mesh_uw.Gamma 
+penalty = 1e6
+
+stokes.add_natural_bc(penalty * Gamma.dot(v_uw.sym) *  Gamma, "Upper")
+stokes.add_natural_bc(penalty * Gamma.dot(v_uw.sym) *  Gamma, "Lower")
+stokes.add_natural_bc(-rho * unit_rvec, "Internal")
+
+stokes.bodyforce = sympy.Matrix([0,0])
+
+
+
+
+
+
+
mesh_uw.dm.view()
+
+
+
+
+
+
+
if uw.mpi.size == 1:
+    pvmesh = vis.mesh_to_pv_mesh(mesh_uw)
+    pvmesh.point_data["rho"] = vis.scalar_fn_to_pv_points(pvmesh, rho * sympy.exp(-1e5 * ((r_uw - r_int) ** 2)))
+
+    print(pvmesh.point_data["rho"].min(), pvmesh.point_data["rho"].max())
+    
+    pl = pv.Plotter(window_size=(750, 750))
+    pl.add_mesh(pvmesh, cmap=cmc.roma.resampled(31), edge_color="Grey",
+                scalars="rho", show_edges=False, use_transparency=False,
+                opacity=1.0, ) # clim=[-1, 1] )
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
# Stokes settings
+
+stokes.tolerance = 1.0e-5
+stokes.petsc_options["ksp_monitor"] = None
+
+stokes.petsc_options["snes_type"] = "newtonls"
+stokes.petsc_options["ksp_type"] = "fgmres"
+
+# stokes.petsc_options.setValue("fieldsplit_velocity_pc_type", "mg")
+# stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_type", "kaskade")
+# stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_cycle_type", "w")
+
+stokes.petsc_options["fieldsplit_velocity_mg_coarse_pc_type"] = "svd"
+
+# stokes.petsc_options[f"fieldsplit_velocity_ksp_type"] = "fcg"
+# stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_type"] = "chebyshev"
+# stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_max_it"] = 5
+# stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_converged_maxits"] = None
+
+# gasm is super-fast ... but mg seems to be bulletproof
+# gamg is toughest wrt viscosity
+
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "gamg")
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "additive")
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v")
+
+# # # mg, multiplicative - very robust ... similar to gamg, additive
+
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "mg")
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "multiplicative")
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v")
+
+
+
+
+
+
+
stokes._setup_pointwise_functions()
+stokes._setup_discretisation()
+stokes._setup_solver()
+
+
+
+
+
+
+
stokes.solve(verbose=True)
+
+
+
+
+
+
+
if uw.mpi.size == 1 and plot_ana:
+
+    with mesh_uw.access(v_uw, p_uw, v_err, p_err):
+            # velocities
+            r = uw.function.evalf(r_uw, v_err.coords)
+            for i, coord in enumerate(v_err.coords):
+                if r[i]>r_int:
+                    v_err.data[i] = v_uw.data[i] - solution_above.velocity_cartesian(coord)
+                else:
+                    v_err.data[i] = v_uw.data[i] - solution_below.velocity_cartesian(coord)
+            
+            
+            # pressure 
+            r = uw.function.evalf(r_uw, p_err.coords)
+            for i, coord in enumerate(p_err.coords):
+                if r[i]>r_int:
+                    p_err.data[i] = p_uw.data[i] - solution_above.pressure_cartesian(coord)
+                else:
+                    p_err.data[i] = p_uw.data[i] - solution_below.pressure_cartesian(coord)
+
+
+
+
+
+
+
# plotting velocities from uw
+if uw.mpi.size == 1:
+    pvmesh = vis.mesh_to_pv_mesh(mesh_uw)
+    pvmesh.point_data["V_uw"] = vis.vector_fn_to_pv_points(pvmesh, v_uw.sym)
+    pvmesh.point_data["Vmag_uw"] = vis.scalar_fn_to_pv_points(pvmesh, sympy.sqrt(v_uw.sym.dot(v_uw.sym)))
+
+    print(pvmesh.point_data["Vmag_uw"].min(), pvmesh.point_data["Vmag_uw"].max())
+    
+    velocity_points = vis.meshVariable_to_pv_cloud(v_uw)
+    velocity_points.point_data["V_uw"] = vis.vector_fn_to_pv_points(velocity_points, v_uw.sym)
+
+    pl = pv.Plotter(window_size=(750, 750))
+    pl.add_mesh(pvmesh, cmap=cmc.lapaz.resampled(11), edge_color="Grey",
+                scalars="Vmag_uw", show_edges=False, use_transparency=False,
+                opacity=0.7,)# clim=[0., 0.05] )
+    pl.add_arrows(velocity_points.points[::10], velocity_points.point_data["V_uw"][::10], mag=10, color='k')
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
# plotting error in velocities
+if uw.mpi.size == 1:
+    pvmesh = vis.mesh_to_pv_mesh(mesh_uw)
+    
+    pvmesh.point_data["V_err"] = vis.vector_fn_to_pv_points(pvmesh, v_err.sym)
+    pvmesh.point_data["Vmag_err"] = vis.scalar_fn_to_pv_points(pvmesh, sympy.sqrt(v_err.sym.dot(v_err.sym)))
+
+    print(pvmesh.point_data["Vmag_err"].min(), pvmesh.point_data["Vmag_err"].max())
+    
+    velocity_points = vis.meshVariable_to_pv_cloud(v_uw)
+    velocity_points.point_data["V_err"] = vis.vector_fn_to_pv_points(velocity_points, v_err.sym)
+
+    pl = pv.Plotter(window_size=(750, 750))
+    pl.add_mesh(pvmesh, cmap=cmc.lapaz.resampled(11), edge_color="Grey",
+                scalars="Vmag_err", show_edges=False, use_transparency=False,
+                opacity=0.7, clim=[0., 0.005] )
+    pl.add_arrows(velocity_points.points[::50], velocity_points.point_data["V_err"][::50], mag=100, color='k')
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
# plotting pressure from uw
+if uw.mpi.size == 1:
+    pvmesh = vis.mesh_to_pv_mesh(mesh_uw)
+    pvmesh.point_data["P_uw"] = vis.scalar_fn_to_pv_points(pvmesh, p_uw.sym)
+
+    print(pvmesh.point_data["P_uw"].min(), pvmesh.point_data["P_uw"].max())
+   
+    pl = pv.Plotter(window_size=(750, 750))
+    pl.add_mesh(pvmesh, cmap=cmc.vik.resampled(41), edge_color="Grey",
+                scalars="P_uw", show_edges=False, use_transparency=False,
+                opacity=1.0, clim=[-0.85, 0.85] )
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
# plotting error in uw
+if uw.mpi.size == 1:
+    pvmesh = vis.mesh_to_pv_mesh(mesh_uw)
+    pvmesh.point_data["P_err"] = vis.scalar_fn_to_pv_points(pvmesh, p_err.sym)
+
+    print(pvmesh.point_data["P_err"].min(), pvmesh.point_data["P_err"].max())
+   
+    pl = pv.Plotter(window_size=(750, 750))
+    pl.add_mesh(pvmesh, cmap=cmc.vik.resampled(41), edge_color="Grey",
+                scalars="P_err", show_edges=False, use_transparency=False,
+                opacity=1.0, clim=[-0.085, 0.085] )
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
# pressure error (L2 norm)
+p_err.stats()[5]/p_ana.stats()[5]
+
+
+
+
+
+
+
mesh_uw.dm.view()
+
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Utilities/Ex_Stokes_Annulus_Benchmark_Thieulot.html b/main/Notebooks/Examples-Utilities/Ex_Stokes_Annulus_Benchmark_Thieulot.html new file mode 100644 index 0000000..ecbd9b0 --- /dev/null +++ b/main/Notebooks/Examples-Utilities/Ex_Stokes_Annulus_Benchmark_Thieulot.html @@ -0,0 +1,1019 @@ + + + + + + + + + + + Annulus Benchmark: Isoviscous Incompressible Stokes — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Annulus Benchmark: Isoviscous Incompressible Stokes

+ +
+ +
+
+ + + + +
+ +
+

Annulus Benchmark: Isoviscous Incompressible Stokes#

+
+

Case: Infinitely thin density anomaly at \(r = r'\)#

+
+

Benchmark paper#

+

Author: Thyagarajulu Gollapalli

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import underworld3 as uw
+from underworld3.systems import Stokes
+
+import numpy as np
+import sympy
+from sympy import lambdify
+import os
+import matplotlib.pyplot as plt
+import cmcrameri.cm as cmc
+import assess
+
+
+
+
+
+
+
os.environ["SYMPY_USE_CACHE"] = "no"
+
+
+
+
+
+
+
if uw.mpi.size == 1:
+    import pyvista as pv
+    import underworld3.visualisation as vis
+
+
+
+
+
+
+
# mesh options
+res = 1/32
+res_int_fac = 1/2
+r_o = 2.0
+r_int = 1.8
+r_i = 1.0
+
+
+
+
+
+
+
# visualize analytical solutions
+plot_ana = True
+
+
+
+
+
+
+
+

Analytical Solution#

+
+
+
if plot_ana:
+    n = 2 # wave number
+    solution_above = assess.CylindricalStokesSolutionDeltaFreeSlip(n, +1, Rp=r_o, Rm=r_i, rp=r_int, nu=1.0, g=-1.0)
+    solution_below = assess.CylindricalStokesSolutionDeltaFreeSlip(n, -1, Rp=r_o, Rm=r_i, rp=r_int, nu=1.0, g=-1.0)
+
+
+
+
+
+
+
if plot_ana:
+    mesh_ana = uw.meshing.AnnulusInternalBoundary(radiusOuter=r_o, 
+                                                  radiusInternal=r_int, 
+                                                  radiusInner=r_i, 
+                                                  cellSize_Inner=res,
+                                                  cellSize_Internal=res*res_int_fac,
+                                                  cellSize_Outer=res,)
+
+
+
+
+
+
+
if plot_ana:
+    v_ana = uw.discretisation.MeshVariable(r"\mathbf{u_a}", mesh_ana, 2, degree=2)
+    p_ana = uw.discretisation.MeshVariable(r"p_a", mesh_ana, 1, degree=1)
+    rho_ana = uw.discretisation.MeshVariable(r"rho_a", mesh_ana, 1, degree=1)
+
+
+
+
+
+
+
if uw.mpi.size == 1 and plot_ana:
+
+    pvmesh = vis.mesh_to_pv_mesh(mesh_ana)
+   
+    pl = pv.Plotter(window_size=(750, 750))
+
+    pl.add_mesh(pvmesh, edge_color="Grey", show_edges=True, use_transparency=False, opacity=1.0, )
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
if plot_ana:
+    r_ana, th_ana = mesh_ana.CoordinateSystem.xR
+
+
+
+
+
+
+
if plot_ana:
+    with mesh_ana.access(v_ana, p_ana, rho_ana):
+        # velocities
+        r = uw.function.evalf(r_ana, v_ana.coords)
+        for i, coord in enumerate(v_ana.coords):
+            if r[i]>r_int:
+                v_ana.data[i] = solution_above.velocity_cartesian(coord)
+            else:
+                v_ana.data[i] = solution_below.velocity_cartesian(coord)
+        
+        
+        # pressure 
+        r = uw.function.evalf(r_ana, p_ana.coords)
+        for i, coord in enumerate(p_ana.coords):
+            if r[i]>r_int:
+                p_ana.data[i] = solution_above.pressure_cartesian(coord)
+            else:
+                p_ana.data[i] = solution_below.pressure_cartesian(coord)
+    
+        # density
+        r = uw.function.evalf(r_ana, rho_ana.coords)
+        for i, coord in enumerate(rho_ana.coords):
+            if r[i]>r_int:
+                rho_ana.data[i] = solution_above.radial_stress_cartesian(coord)
+            else:
+                rho_ana.data[i] = solution_below.radial_stress_cartesian(coord)
+
+
+
+
+
+
+
if uw.mpi.size == 1 and plot_ana:
+    pvmesh_ana = vis.mesh_to_pv_mesh(mesh_ana)
+    pvmesh_ana.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh_ana, v_ana.sym)
+    pvmesh_ana.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh_ana, 
+                                                               sympy.sqrt(v_ana.sym.dot(v_ana.sym)))
+    
+    print(pvmesh_ana.point_data["Vmag"].min(), pvmesh_ana.point_data["Vmag"].max())
+    
+    velocity_points_ana = vis.meshVariable_to_pv_cloud(v_ana)
+    velocity_points_ana.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points_ana, v_ana.sym)
+    
+    pl = pv.Plotter(window_size=(750, 750))
+    pl.add_mesh(pvmesh_ana, cmap=cmc.lapaz.resampled(11), edge_color="Grey",
+                scalars="Vmag", show_edges=False, use_transparency=False,
+                opacity=0.7, clim=[0., 0.05] )
+    pl.add_arrows(velocity_points_ana.points[::10], velocity_points_ana.point_data["V"][::10], 
+                  mag=5, color='k')
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
if uw.mpi.size == 1 and plot_ana:
+    pvmesh_ana = vis.mesh_to_pv_mesh(mesh_ana)
+    pvmesh_ana.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh_ana, p_ana.sym)
+
+    print(pvmesh_ana.point_data["P"].min(), pvmesh_ana.point_data["P"].max())
+   
+    pl = pv.Plotter(window_size=(750, 750))
+    pl.add_mesh(pvmesh_ana, cmap=cmc.vik.resampled(41), edge_color="Grey",
+                scalars="P", show_edges=False, use_transparency=False,
+                opacity=1.0, clim=[-0.65, 0.65] )
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
if uw.mpi.size == 1 and plot_ana:
+    pvmesh_ana = vis.mesh_to_pv_mesh(mesh_ana)
+    pvmesh_ana.point_data["rho"] = vis.scalar_fn_to_pv_points(pvmesh_ana, rho_ana.sym)
+
+    print(pvmesh_ana.point_data["rho"].min(), pvmesh_ana.point_data["rho"].max())
+    
+    pl = pv.Plotter(window_size=(750, 750))
+    pl.add_mesh(pvmesh_ana, cmap=cmc.roma.resampled(41), edge_color="Grey",
+                scalars="rho", show_edges=False, use_transparency=False,
+                opacity=1.0, clim=[-0.8, 0.8] )
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+

Create Mesh for Numerical Solution#

+
+
+
# mesh
+mesh_uw = uw.meshing.AnnulusInternalBoundary(radiusOuter=r_o, 
+                                             radiusInternal=r_int, 
+                                             radiusInner=r_i, 
+                                             cellSize_Inner=res,
+                                             cellSize_Internal=res*res_int_fac,
+                                             cellSize_Outer=res,)
+
+
+
+
+
+
+
# mesh variables
+v_uw = uw.discretisation.MeshVariable(r"\mathbf{u}", mesh_uw, 2, degree=2)
+p_uw = uw.discretisation.MeshVariable(r"p", mesh_uw, 1, degree=1)
+v_err = uw.discretisation.MeshVariable(r"\mathbf{u_e}", mesh_uw, 2, degree=2)
+p_err = uw.discretisation.MeshVariable(r"p_e", mesh_uw, 1, degree=1)
+
+
+
+
+
+
+
# Some useful coordinate stuff
+unit_rvec = mesh_uw.CoordinateSystem.unit_e_0
+r_uw, th_uw = mesh_uw.CoordinateSystem.xR
+
+
+
+
+
+
+
# Create Stokes object
+
+stokes = Stokes(mesh_uw, velocityField=v_uw, pressureField=p_uw, solver_name="stokes")
+
+stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes.constitutive_model.Parameters.viscosity = 1.0
+stokes.saddle_preconditioner = 1.0
+
+rho = sympy.cos(n*th_uw) * sympy.exp(-1e5 * ((r_uw - r_int) ** 2)) 
+
+penalty = 1e7
+Gamma = mesh_uw.Gamma
+stokes.add_natural_bc(penalty * Gamma.dot(v_uw.sym) *  Gamma, "Upper")
+stokes.add_natural_bc(penalty * Gamma.dot(v_uw.sym) *  Gamma, "Lower")
+stokes.add_natural_bc(-rho * unit_rvec, "Internal")
+
+stokes.bodyforce = sympy.Matrix([0,0])
+
+
+
+
+
+
+
if uw.mpi.size == 1:
+    pvmesh = vis.mesh_to_pv_mesh(mesh_uw)
+    pvmesh.point_data["rho"] = vis.scalar_fn_to_pv_points(pvmesh, rho)
+
+    print(pvmesh.point_data["rho"].min(), pvmesh.point_data["rho"].max())
+    
+    pl = pv.Plotter(window_size=(750, 750))
+    pl.add_mesh(pvmesh, cmap=cmc.roma.resampled(31), edge_color="Grey",
+                scalars="rho", show_edges=False, use_transparency=False,
+                opacity=1.0, clim=[-1, 1] )
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
# Stokes settings
+
+stokes.tolerance = 1.0e-6
+stokes.petsc_options["ksp_monitor"] = None
+
+stokes.petsc_options["snes_type"] = "newtonls"
+stokes.petsc_options["ksp_type"] = "fgmres"
+
+# stokes.petsc_options.setValue("fieldsplit_velocity_pc_type", "mg")
+stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_type", "kaskade")
+stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_cycle_type", "w")
+
+stokes.petsc_options["fieldsplit_velocity_mg_coarse_pc_type"] = "svd"
+
+stokes.petsc_options[f"fieldsplit_velocity_ksp_type"] = "fcg"
+stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_type"] = "chebyshev"
+stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_max_it"] = 5
+stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_converged_maxits"] = None
+
+# gasm is super-fast ... but mg seems to be bulletproof
+# gamg is toughest wrt viscosity
+
+stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "gamg")
+stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "additive")
+stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v")
+
+# # # mg, multiplicative - very robust ... similar to gamg, additive
+
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "mg")
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "multiplicative")
+# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v")
+
+
+
+
+
+
+
stokes.solve()
+
+
+
+
+
+
+
with mesh_uw.access(v_uw, p_uw, v_err, p_err):
+        # velocities
+        r = uw.function.evalf(r_uw, v_err.coords)
+        for i, coord in enumerate(v_err.coords):
+            if r[i]>r_int:
+                v_err.data[i] = v_uw.data[i] - solution_above.velocity_cartesian(coord)
+            else:
+                v_err.data[i] = v_uw.data[i] - solution_below.velocity_cartesian(coord)
+        
+        
+        # pressure 
+        r = uw.function.evalf(r_uw, p_err.coords)
+        for i, coord in enumerate(p_err.coords):
+            if r[i]>r_int:
+                p_err.data[i] = p_uw.data[i] - solution_above.pressure_cartesian(coord)
+            else:
+                p_err.data[i] = p_uw.data[i] - solution_below.pressure_cartesian(coord)
+
+
+
+
+
+
+
# plotting velocities from uw
+if uw.mpi.size == 1:
+    pvmesh = vis.mesh_to_pv_mesh(mesh_uw)
+    pvmesh.point_data["V_uw"] = vis.vector_fn_to_pv_points(pvmesh, v_uw.sym)
+    pvmesh.point_data["Vmag_uw"] = vis.scalar_fn_to_pv_points(pvmesh, sympy.sqrt(v_uw.sym.dot(v_uw.sym)))
+
+    print(pvmesh.point_data["Vmag_uw"].min(), pvmesh.point_data["Vmag_uw"].max())
+    
+    velocity_points = vis.meshVariable_to_pv_cloud(v_uw)
+    velocity_points.point_data["V_uw"] = vis.vector_fn_to_pv_points(velocity_points, v_uw.sym)
+
+    pl = pv.Plotter(window_size=(750, 750))
+    pl.add_mesh(pvmesh, cmap=cmc.lapaz.resampled(11), edge_color="Grey",
+                scalars="Vmag_uw", show_edges=False, use_transparency=False,
+                opacity=0.1, clim=[0., 0.05] )
+    pl.add_arrows(velocity_points.points[::10], velocity_points.point_data["V_uw"][::10], mag=1e1, color='k')
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
# plotting errror in velocities
+if uw.mpi.size == 1:
+    pvmesh = vis.mesh_to_pv_mesh(mesh_uw)
+    
+    pvmesh.point_data["V_err"] = vis.vector_fn_to_pv_points(pvmesh, v_err.sym)
+    pvmesh.point_data["Vmag_err"] = vis.scalar_fn_to_pv_points(pvmesh, sympy.sqrt(v_err.sym.dot(v_err.sym)))
+
+    print(pvmesh.point_data["Vmag_err"].min(), pvmesh.point_data["Vmag_err"].max())
+    
+    velocity_points = vis.meshVariable_to_pv_cloud(v_uw)
+    velocity_points.point_data["V_err"] = vis.vector_fn_to_pv_points(velocity_points, v_err.sym)
+
+    pl = pv.Plotter(window_size=(750, 750))
+    pl.add_mesh(pvmesh, cmap=cmc.lapaz.resampled(11), edge_color="Grey",
+                scalars="Vmag_err", show_edges=False, use_transparency=False,
+                opacity=0.7, clim=[0., 0.005] )
+    pl.add_arrows(velocity_points.points[::50], velocity_points.point_data["V_err"][::50], mag=1e-1, color='k')
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
# plotting pressure from uw
+if uw.mpi.size == 1:
+    pvmesh = vis.mesh_to_pv_mesh(mesh_uw)
+    pvmesh.point_data["P_uw"] = vis.scalar_fn_to_pv_points(pvmesh, p_uw.sym)
+
+    print(pvmesh.point_data["P_uw"].min(), pvmesh.point_data["P_uw"].max())
+   
+    pl = pv.Plotter(window_size=(750, 750))
+    pl.add_mesh(pvmesh, cmap=cmc.vik.resampled(41), edge_color="Grey",
+                scalars="P_uw", show_edges=False, use_transparency=False,
+                opacity=1.0, clim=[-0.85, 0.85] )
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
# plotting error in uw
+if uw.mpi.size == 1:
+    pvmesh = vis.mesh_to_pv_mesh(mesh_uw)
+    pvmesh.point_data["P_err"] = vis.scalar_fn_to_pv_points(pvmesh, p_err.sym)
+
+    print(pvmesh.point_data["P_err"].min(), pvmesh.point_data["P_err"].max())
+   
+    pl = pv.Plotter(window_size=(750, 750))
+    pl.add_mesh(pvmesh, cmap=cmc.vik.resampled(41), edge_color="Grey",
+                scalars="P_err", show_edges=False, use_transparency=False,
+                opacity=1.0, clim=[-0.085, 0.085] )
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
# pressure error (L2 norm)
+p_err.stats()[5]/p_ana.stats()[5]
+
+
+
+
+
+
+
mesh_uw.dm.view()
+
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Utilities/Ex_SurfaceIntegrals_BCs.html b/main/Notebooks/Examples-Utilities/Ex_SurfaceIntegrals_BCs.html new file mode 100644 index 0000000..7ce6dae --- /dev/null +++ b/main/Notebooks/Examples-Utilities/Ex_SurfaceIntegrals_BCs.html @@ -0,0 +1,881 @@ + + + + + + + + + + + Surface integral / natural boundary conditions — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Surface integral / natural boundary conditions

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

Surface integral / natural boundary conditions#

+
+
+
import underworld3 as uw
+import numpy as np
+import sympy
+import os
+import sys
+import petsc4py
+import matplotlib.pyplot as plt
+
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
minX = -1.0
+maxX = 1.0
+minY = -1.0
+maxY = 1.0
+
+resX = 4
+resY = 4
+
+cell_height = maxY / resY
+cell_width = maxX / resX
+
+meshQuad = uw.meshing.StructuredQuadBox(
+        elementRes=(resX, resY), 
+        minCoords=(minX, minY), 
+        maxCoords=(maxX, maxY), qdegree=3)
+
+meshTri = uw.meshing.UnstructuredSimplexBox(
+        regular=True,
+        cellSize=1/2, 
+        minCoords=(minX, minY), 
+        maxCoords=(maxX, maxY), qdegree=3)
+
+mesh = meshTri
+x,y = mesh.X
+
+
+
+
+
+
+
uw.systems.Stokes.view()
+
+
+
+
+
+
+

+stokes0 = uw.systems.Stokes(mesh, solver_name="Stokes0")
+stokes0.constitutive_model = uw.constitutive_models.ViscousFlowModel
+
+v0 = stokes0.Unknowns.u
+p0 = stokes0.Unknowns.p
+
+stokes0.add_essential_bc( [0.,0.], "Bottom")  # no slip on the base
+stokes0.add_essential_bc( [0.,sympy.oo], "Left")     # free slip Left/Right
+stokes0.add_essential_bc( [0.,sympy.oo], "Right")     # no slip on the top
+stokes0.bodyforce = sympy.Matrix([0, sympy.sin(x*sympy.pi)])
+
+### see the SNES output
+stokes0.petsc_options["snes_converged_reason"] = None
+stokes0.petsc_options["snes_monitor_short"] = None
+stokes0.tolerance = 1.0e-6
+
+stokes0.solve()
+
+
+
+
+
+
+
def JacViewer(stokes_solver):
+
+    Jac, JacP, _ = stokes_solver.snes.getJacobian()
+    ii,jj = Jac.getSize()
+
+    ## Let's take a look (Sorry, Matt, using python for this)
+
+    Jm = Jac.getValues(range(0,ii),range(0,jj))
+    JPm = JacP.getValues(range(0,ii),range(0,jj))
+
+    fig = plt.figure(figsize=(12,6), facecolor="none")
+    ax  = plt.subplot(131)   # 2x1 array of plots, ax refers to the 1st of them
+    ax2  = plt.subplot(132)   # 1x1 array of plots, ax refers to the 1st of them
+    ax3  = plt.subplot(133)   # 1x1 array of plots, ax refers to the 1st of them
+    
+    ax.imshow(Jm, vmin=-1, vmax=1, cmap="coolwarm")
+    ax2.imshow(JPm, vmin=-1, vmax=1, cmap="coolwarm")
+    ax3.imshow(JPm-Jm, vmin=-0.01, vmax=0.01, cmap="coolwarm")
+    
+def JacDiffViewer(stokes_solver1, stokes_solver2):
+
+    Jac1, JacP1, _ = stokes_solver1.snes.getJacobian()
+    ii,jj = Jac1.getSize()
+
+    Jac2, JacP2, _ = stokes_solver2.snes.getJacobian()
+
+    
+    ## Let's take a look (Sorry, Matt, using python for this)
+
+    Jm1 = Jac1.getValues(range(0,ii),range(0,jj))
+    JPm1 = JacP1.getValues(range(0,ii),range(0,jj))
+    
+    Jm2 = Jac2.getValues(range(0,ii),range(0,jj))
+    JPm2 = JacP2.getValues(range(0,ii),range(0,jj))
+
+    fig = plt.figure(figsize=(12,6), facecolor="none")
+    ax  = plt.subplot(121)   # 2x1 array of plots, ax refers to the 1st of them
+    ax2  = plt.subplot(122)   # 1x1 array of plots, ax refers to the 1st of them
+    # ax3  = plt.subplot(133)   # 1x1 array of plots, ax refers to the 1st of them
+
+    JmaxD = np.abs(Jm1-Jm2).max()
+    JPmaxD = np.abs(JPm1-JPm2).max()
+
+    print(f"Jac: min {(Jm1-Jm2).min()} / max {(Jm1-Jm2).max()}", flush=True) 
+    print(f"JacP: min {(JPm1-JPm2).min()} / max {(JPm1-JPm2).max()}", flush=True) 
+    
+    ax.imshow(Jm1-Jm2,  vmin=-JmaxD, vmax=JmaxD, cmap="coolwarm")
+    ax2.imshow(JPm1-JPm2, vmin=-JPmaxD, vmax=JPmaxD,  cmap="coolwarm")
+
+JacViewer(stokes0)
+
+
+
+
+
+
+
## Now try adding a null natural bc - Expect no changes to J, P though 
+## the surface terms should be active if we check the debug output
+
+stokes1 = uw.systems.Stokes(mesh, solver_name="Stokes1")
+stokes1.constitutive_model = uw.constitutive_models.ViscousFlowModel
+
+v1 = stokes1.Unknowns.u
+p1 = stokes1.Unknowns.p
+
+stokes1.petsc_options["snes_converged_reason"] = None
+stokes1.petsc_options["snes_monitor_short"] = None
+stokes1.tolerance = 1.0e-6
+
+stokes1.add_essential_bc( [0.,0.], "Bottom")         # no slip on the base
+stokes1.add_essential_bc( [0.,sympy.oo], "Left")     # free slip Left/Right
+stokes1.add_essential_bc( [0.,sympy.oo], "Right")    # free slip Left/Right
+stokes1.add_natural_bc( [0.,0.], "Top")              # Top is open (still)
+
+stokes1.bodyforce = sympy.Matrix([0, sympy.sin(x*sympy.pi)])
+
+
+
+
+
+
+
stokes1._setup_pointwise_functions()
+stokes1._setup_discretisation()
+stokes1._setup_problem_description()
+
+
+
+
+
+
+

+stokes1.solve(verbose=False, debug=False, zero_init_guess=True, picard=0, _force_setup=False)
+
+
+
+
+
+
+
JacViewer(stokes1)
+JacDiffViewer(stokes0, stokes1)
+
+
+
+
+
+
+
## Now try adding a constant natural bc - Still expect no changes to J, P though 
+## the surface terms should be active if we check the debug output
+
+stokes2 = uw.systems.Stokes(mesh, solver_name="Stokes2")
+stokes2.constitutive_model = uw.constitutive_models.ViscousFlowModel
+
+v2 = stokes2.Unknowns.u
+p2 = stokes2.Unknowns.p
+
+stokes2.petsc_options["snes_converged_reason"] = None
+stokes2.petsc_options["snes_monitor_short"] = None
+stokes2.tolerance = 1.0e-6
+
+stokes2.add_essential_bc( [0.,0.], "Bottom")         # no slip on the base
+stokes2.add_essential_bc( [0.,sympy.oo], "Left")     # free slip Left/Right
+stokes2.add_essential_bc( [0.,sympy.oo], "Right")    # free slip Left/Right
+stokes2.add_natural_bc( [1.,0.], "Top")              # Top is driven (constant)
+
+stokes2.bodyforce = sympy.Matrix([0, sympy.sin(x*sympy.pi)])
+
+stokes2.solve(verbose=False, debug=False, zero_init_guess=True, picard=5, _force_setup=False)
+
+
+
+
+
+
+
JacViewer(stokes2)
+JacDiffViewer(stokes0, stokes2)
+
+
+
+
+
+
+
## Now try adding a constant natural bc. J, P should be unchanged as this is 
+## linear, but we should see a different RHS reflected in the SNES norms
+
+stokes3 = uw.systems.Stokes(mesh, solver_name="Stokes3")
+stokes3.constitutive_model = uw.constitutive_models.ViscousFlowModel
+stokes3.petsc_options.setValue("snes_monitor", None)
+
+v3 = stokes3.Unknowns.u
+p3 = stokes3.Unknowns.p
+
+stokes3.add_essential_bc( [0.,0.], "Bottom")         # no slip on the base
+stokes3.add_essential_bc( [0.,sympy.oo], "Left")     # free slip Left/Right
+stokes3.add_essential_bc( [0.,sympy.oo], "Right")    # free slip Left/Right
+# stokes3.add_natural_bc(   [0.,1000000*v3.sym[1]], "Top")              # Top "free slip / penalty"
+
+Gamma = mesh.Gamma # sympy.Piecewise((mesh.Gamma, x < 0.5), mesh.CoordinateSystem.unit_j
+Gamma = sympy.Matrix([0,1])
+stokes3.add_natural_bc( 1.0e8 *  Gamma.dot(v3.sym) * Gamma, "Top")              # Top "free slip / penalty"
+
+stokes3.bodyforce = sympy.Matrix([0, sympy.sin(x*sympy.pi)])
+
+# Set verbose / debug to True to see the functions being executed
+stokes3.solve(verbose=False, debug=False, zero_init_guess=True, picard=2)
+
+
+
+
+
+
+
JacViewer(stokes3)
+JacDiffViewer(stokes3, stokes0)
+
+
+
+
+
+
+
## Validation step - Did the penalty approach actually work
+
+stokesN = uw.systems.Stokes(mesh, solver_name="StokesN")
+stokesN.constitutive_model = uw.constitutive_models.ViscousFlowModel
+vN = stokesN.Unknowns.u
+
+stokesN.tolerance = 1.0e-6
+stokesN.petsc_options.setValue("snes_monitor", None)
+stokesN.petsc_options.setValue("ksp_monitor", None)
+
+stokesN.add_essential_bc( [0.,0.], "Bottom")         # no slip on the base
+stokesN.add_essential_bc( [0.,sympy.oo], "Left")     # free slip Left/Right
+stokesN.add_essential_bc( [0.,sympy.oo], "Right")    # free slip Left/Right
+stokesN.add_essential_bc( [sympy.oo, 0.], "Top")     # Top "free slip / penalty"
+
+
+stokesN.bodyforce = sympy.Matrix([0, sympy.sin(x*sympy.pi)])
+
+stokesN.solve(verbose=False, debug=False, zero_init_guess=True)
+
+
+
+
+
+
+
# Visuals
+
+# This creates a plot of the true free-surface solution, the penalized velocity solution, 
+# and their difference
+
+
+import underworld3 as uw
+import pyvista as pv
+import underworld3.visualisation
+
+pl = pv.Plotter(window_size=(1000, 500))
+
+pvmesh = uw.visualisation.mesh_to_pv_mesh(mesh)
+pvmesh.point_data["V0"] = uw.visualisation.vector_fn_to_pv_points(pvmesh, vN.sym)
+pvmesh.point_data["V3"] = uw.visualisation.vector_fn_to_pv_points(pvmesh, v3.sym)
+pvmesh.point_data["P"] = uw.visualisation.scalar_fn_to_pv_points(pvmesh, p3.sym)
+# pvmesh.point_data["Vmag"] = uw.visualisation.scalar_fn_to_pv_points(pvmesh, v.sym.dot(v.sym))
+
+velocity_points = underworld3.visualisation.meshVariable_to_pv_cloud(v0)
+velocity_points.point_data["V0"] = uw.visualisation.vector_fn_to_pv_points(velocity_points, vN.sym)
+velocity_points.point_data["V3"] = uw.visualisation.vector_fn_to_pv_points(velocity_points, v3.sym)
+
+pl.add_mesh(
+    pvmesh,
+    cmap="coolwarm",
+    edge_color="Black",
+    show_edges=True,
+    scalars="P",
+    use_transparency=False,
+    opacity=1.0,
+)
+
+
+pl.add_arrows(velocity_points.points, velocity_points.point_data["V3"]-velocity_points.point_data["V0"], mag=100000, opacity=0.75)
+pl.add_arrows(velocity_points.points+(0.01,0.0,0.0), velocity_points.point_data["V3"], mag=1.0, opacity=0.75)
+pl.add_arrows(velocity_points.points+(0.00,0.0,0.0), velocity_points.point_data["V0"], mag=1.0, opacity=0.75)
+# pl.add_mesh(pvstream)
+
+pl.camera.SetPosition(0.75, 0.2, 1.5)
+pl.camera.SetFocalPoint(0.75, 0.2, 0.0)
+pl.camera.SetClippingRange(1.0, 8.0)
+
+# pl.remove_scalar_bar("Omega")
+# pl.remove_scalar_bar("mag")
+# pl.remove_scalar_bar("V")
+
+pl.show()
+
+
+
+
+
+
+

#

+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Utilities/Ex_Tensor_Variables_etc.html b/main/Notebooks/Examples-Utilities/Ex_Tensor_Variables_etc.html new file mode 100644 index 0000000..912c35e --- /dev/null +++ b/main/Notebooks/Examples-Utilities/Ex_Tensor_Variables_etc.html @@ -0,0 +1,1127 @@ + + + + + + + + + + + Examples with General Mesh variable manipulation — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Examples with General Mesh variable manipulation

+ +
+
+ +
+
+
+ + + + +
+ +
+

Examples with General Mesh variable manipulation#

+

We introduce the notion of an IndexSwarmVariable which automatically generates masks for a swarm +variable that consists of discrete level values (integers).

+

For a variable \(M\), the mask variables are \(\left\{ M^0, M^1, M^2 \ldots M^N \right\}\) where \(N\) is the number of indices (e.g. material types) on the variable. This value must be defined in advance.

+

The masks are orthogonal in the sense that \(M^i * M^j = 0\) if \(i \ne j\), and they are complete in the sense that \(\sum_i M^i = 1\) at all points.

+

The masks are implemented as continuous mesh variables (the user can specify the interpolation order) and so they are also differentiable (once).

+
+
+
# to fix trame issue
+import nest_asyncio
+nest_asyncio.apply()
+
+
+
+
+
+
+
import petsc4py
+from petsc4py import PETSc
+
+import underworld3 as uw
+from underworld3.systems import Stokes
+from underworld3 import function
+
+import numpy as np
+import sympy
+
+
+
+
+
+
+
meshbox = uw.meshing.UnstructuredSimplexBox(
+    minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0), cellSize=1.0 / 32.0
+)
+meshbox.dm.view()
+
+
+
+
+
+
+
import sympy
+
+# Some useful coordinate stuff
+
+x, y = meshbox.CoordinateSystem.X
+
+
+
+
+
+
+
v_soln = uw.discretisation.MeshVariable("U", meshbox, meshbox.dim, degree=2)
+p_soln = uw.discretisation.MeshVariable("P", meshbox, 1, degree=1)
+
+
+
+
+
+
+
sympy.diff(v_soln.sym, x)
+
+
+
+
+
+
+
swarm = uw.swarm.Swarm(mesh=meshbox)
+material = uw.swarm.IndexSwarmVariable("M", swarm, indices=4)
+
+
+
+
+
+
+
swarm.populate(fill_param=5)
+
+
+
+
+
+
+
with swarm.access(material):
+    material.data[...] = 0
+    for i in range(50):
+        cx, cy, r = np.random.random(3)
+        m = np.random.randint(1, 4)
+        r = 0.025 + r * 0.025
+
+        inside = (swarm.data[:, 0] - cx) ** 2 + (swarm.data[:, 1] - cy) ** 2 < r**2
+        material.data[inside] = m
+
+
+
+
+
+
+
material.sym.diff(meshbox.CoordinateSystem.X)
+
+
+
+
+
+
+
material.sym.jacobian(meshbox.X).T
+
+
+
+
+
+
+
meshbox.vector.jacobian(material.sym).T
+
+
+
+
+
+
+
v_soln.sym.jacobian(meshbox.CoordinateSystem.X)
+
+
+
+
+
+
+
mat_density = np.array([1, 10, 100, 1000])
+density = (
+    mat_density[0] * material.sym[0]
+    + mat_density[1] * material.sym[1]
+    + mat_density[2] * material.sym[2]
+    + mat_density[3] * material.sym[3]
+)
+
+
+
+
+
+
+
mat_viscosity = np.array([1, 10, 100, 1000])
+viscosity = (
+    mat_viscosity[0] * material.sym[0]
+    + mat_viscosity[1] * material.sym[1]
+    + mat_viscosity[2] * material.sym[2]
+    + mat_viscosity[3] * material.sym[3]
+)
+
+
+
+
+
+
+
import numpy as np
+import pyvista as pv
+import vtk
+
+pv.global_theme.background = "white"
+pv.global_theme.window_size = [750, 750]
+pv.global_theme.antialiasing = True
+pv.global_theme.jupyter_backend = "trame"
+pv.global_theme.smooth_shading = True
+
+
+meshbox.vtk("tmp_box.vtk")
+pvmesh = pv.read("tmp_box.vtk")
+
+with swarm.access():
+    points = np.zeros((swarm.data.shape[0], 3))
+    points[:, 0] = swarm.data[:, 0]
+    points[:, 1] = swarm.data[:, 1]
+    points[:, 2] = 0.0
+
+point_cloud = pv.PolyData(points)
+
+with meshbox.access():
+    pvmesh.point_data["M0"] = uw.function.evaluate(material.sym[0], meshbox.data)
+    pvmesh.point_data["M1"] = uw.function.evaluate(material.sym[1], meshbox.data)
+    pvmesh.point_data["M2"] = uw.function.evaluate(material.sym[2], meshbox.data)
+    pvmesh.point_data["M3"] = uw.function.evaluate(material.sym[3], meshbox.data)
+    pvmesh.point_data["M"] = (
+        1.0 * pvmesh.point_data["M1"]
+        + 2.0 * pvmesh.point_data["M2"]
+        + 3.0 * pvmesh.point_data["M3"]
+    )
+
+    pvmesh.point_data["rho"] = uw.function.evaluate(density, meshbox.data)
+    pvmesh.point_data["visc"] = uw.function.evaluate(sympy.log(viscosity), meshbox.data)
+
+
+with swarm.access():
+    point_cloud.point_data["M"] = material.data.copy()
+
+pl = pv.Plotter(notebook=True)
+
+# pl.add_points(point_cloud, color="Black",
+#                   render_points_as_spheres=False,
+#                   point_size=2.5, opacity=0.75)
+
+
+pl.add_mesh(
+    pvmesh,
+    cmap="coolwarm",
+    edge_color="Black",
+    show_edges=False,
+    scalars="visc",
+    use_transparency=False,
+    opacity=0.95,
+)
+
+
+# pl.add_mesh(pvmesh, cmap="coolwarm", edge_color="Black", show_edges=True, scalars="M1",
+#                     use_transparency=False, opacity=1.0)
+
+
+pl.show(cpos="xy")
+
+
+
+
+
+
+
ad = uw.systems.AdvDiffusionSwarm(meshbox, t_soln, T1.fn, degree=3, projection=True)
+
+ad._u_star_projector.smoothing = 0.0
+
+ad.add_dirichlet_bc(1.0, "Bottom")
+ad.add_dirichlet_bc(0.0, "Top")
+
+init_t = 0.01 * sympy.sin(5.0 * x) * sympy.sin(np.pi * y) + (1.0 - y)
+
+with meshbox.access(t_0, t_soln):
+    t_0.data[...] = uw.function.evaluate(init_t, t_0.coords).reshape(-1, 1)
+    t_soln.data[...] = t_0.data[...]
+
+with swarm.access(T1):
+    T1.data[...] = uw.function.evaluate(
+        init_t, swarm.particle_coordinates.data
+    ).reshape(-1, 1)
+
+
+
+
+
+
+
# Create Stokes object
+
+stokes = Stokes(
+    meshbox,
+    velocityField=v_soln,
+    pressureField=p_soln,
+    u_degree=v_soln.degree,
+    p_degree=p_soln.degree,
+    solver_name="stokes",
+    verbose=False,
+)
+
+# Set solve options here (or remove default values
+# stokes.petsc_options.getAll()
+stokes.petsc_options.delValue("ksp_monitor")
+
+# Constant visc
+stokes.viscosity = 1.0
+
+# Velocity boundary conditions
+stokes.add_dirichlet_bc((0.0,), "Left", (0,))
+stokes.add_dirichlet_bc((0.0,), "Right", (0,))
+stokes.add_dirichlet_bc((0.0,), "Top", (1,))
+stokes.add_dirichlet_bc((0.0,), "Bottom", (1,))
+
+
+
+
+
+
+
buoyancy_force = 1.0e6 * t_soln.fn
+stokes.bodyforce = meshbox.N.j * buoyancy_force
+
+# check the stokes solve is set up and that it converges
+stokes.solve()
+
+
+
+
+
+
+
# check the projection
+
+
+if uw.mpi.size == 1 and ad.projection:
+
+    import numpy as np
+    import pyvista as pv
+    import vtk
+
+    pv.global_theme.background = "white"
+    pv.global_theme.window_size = [750, 250]
+    pv.global_theme.antialiasing = True
+    pv.global_theme.jupyter_backend = "trame"
+    pv.global_theme.smooth_shading = True
+
+    pv.start_xvfb()
+
+    pvmesh = meshbox.mesh2pyvista(elementType=vtk.VTK_TRIANGLE)
+
+    with meshbox.access():
+        usol = stokes.u.data.copy()
+
+    pvmesh.point_data["mT1"] = uw.function.evaluate(
+        ad._u_star_projected.fn, meshbox.data
+    )
+    pvmesh.point_data["T1"] = uw.function.evaluate(T1.fn, meshbox.data)
+    pvmesh.point_data["dT1"] = uw.function.evaluate(
+        T1.fn - ad._u_star_projected.fn, meshbox.data
+    )
+
+    arrow_loc = np.zeros((stokes.u.coords.shape[0], 3))
+    arrow_loc[:, 0:2] = stokes.u.coords[...]
+
+    arrow_length = np.zeros((stokes.u.coords.shape[0], 3))
+    arrow_length[:, 0:2] = usol[...]
+
+    pl = pv.Plotter()
+
+    # pl.add_mesh(pvmesh,'Black', 'wireframe')
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        scalars="dT1",
+        use_transparency=False,
+        opacity=0.5,
+    )
+
+    # pl.add_arrows(arrow_loc, arrow_length, mag=1.0e-4, opacity=0.5)
+    # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1)
+
+    # pl.add_points(pdata)
+
+    pl.show(cpos="xy")
+
+
+
+
+
+
+
def plot_T_mesh(filename):
+
+    if uw.mpi.size == 1:
+
+        import numpy as np
+        import pyvista as pv
+        import vtk
+
+        pv.global_theme.background = "white"
+        pv.global_theme.window_size = [750, 750]
+        pv.global_theme.antialiasing = True
+        pv.global_theme.jupyter_backend = "trame"
+        pv.global_theme.smooth_shading = False
+        pv.global_theme.camera["viewup"] = [0.0, 1.0, 0.0]
+        pv.global_theme.camera["position"] = [0.0, 0.0, 5.0]
+
+        pvmesh = meshbox.mesh2pyvista(elementType=vtk.VTK_TRIANGLE)
+
+        points = np.zeros((t_soln.coords.shape[0], 3))
+        points[:, 0] = t_soln.coords[:, 0]
+        points[:, 1] = t_soln.coords[:, 1]
+
+        point_cloud = pv.PolyData(points)
+
+        with meshbox.access():
+            point_cloud.point_data["T"] = t_soln.data.copy()
+
+        with swarm.access():
+            points = np.zeros((swarm.data.shape[0], 3))
+            points[:, 0] = swarm.data[:, 0]
+            points[:, 1] = swarm.data[:, 1]
+
+        swarm_point_cloud = pv.PolyData(points)
+
+        with swarm.access():
+            swarm_point_cloud.point_data["T1"] = T1.data.copy()
+
+        with meshbox.access():
+            usol = stokes.u.data.copy()
+
+        pvmesh.point_data["T"] = uw.function.evaluate(t_soln.fn, meshbox.data)
+
+        arrow_loc = np.zeros((stokes.u.coords.shape[0], 3))
+        arrow_loc[:, 0:2] = stokes.u.coords[...]
+
+        arrow_length = np.zeros((stokes.u.coords.shape[0], 3))
+        arrow_length[:, 0:2] = usol[...]
+
+        pl = pv.Plotter()
+
+        pl.add_arrows(arrow_loc, arrow_length, mag=0.00001, opacity=0.75)
+
+        pl.add_points(
+            swarm_point_cloud,  # cmap="RdYlBu_r", scalars="T1",
+            color="Black",
+            render_points_as_spheres=True,
+            clim=[0.0, 1.0],
+            point_size=1.0,
+            opacity=0.5,
+        )
+
+        pl.add_points(
+            point_cloud,
+            cmap="coolwarm",
+            scalars="T",
+            render_points_as_spheres=False,
+            clim=[0.0, 1.0],
+            point_size=10.0,
+            opacity=0.66,
+        )
+
+        # pl.add_mesh(pvmesh, cmap="coolwarm", edge_color="Black",
+        #             show_edges=True, scalars="T",clim=[0.0,1.0],
+        #               use_transparency=False, opacity=0.5)
+
+        pl.remove_scalar_bar("T")
+        # pl.remove_scalar_bar("T1")
+
+        pl.screenshot(
+            filename="{}.png".format(filename),
+            window_size=(1250, 1250),
+            return_img=False,
+        )
+        # pl.show()
+        pl.close()
+
+
+
+
+
+
+
# Convection model / update in time
+
+expt_name = "output/Ra1e6_swarm_pnots"
+
+ad_delta_t = 0.000033  # target
+
+for step in range(0, 250):
+
+    stokes.solve(zero_init_guess=False)
+    stokes_delta_t = 5.0 * stokes.estimate_dt()
+    delta_t = stokes_delta_t
+
+    ad.solve(timestep=delta_t, zero_init_guess=True)
+
+    # update swarm / swarm variables
+
+    with swarm.access(T1):
+        T1.data[:, 0] = uw.function.evaluate(t_soln.fn, swarm.particle_coordinates.data)
+
+    # advect swarm
+    swarm.advection(v_soln.fn, delta_t)
+
+    tstats = t_soln.stats()
+    tstarstats = T1._meshVar.stats()
+
+    if uw.mpi.rank == 0:
+        print("Timestep {}, dt {}".format(step, delta_t))
+        print(tstats[2], tstats[3])
+        print(tstarstats[2], tstarstats[3])
+
+    plot_T_mesh(filename="{}_step_{}".format(expt_name, step))
+
+# savefile = "{}_ts_{}.h5".format(expt_name,step)
+# meshbox.save(savefile)
+# v_soln.save(savefile)
+# t_soln.save(savefile)
+# meshbox.generate_xdmf(savefile)
+
+
+
+
+

savefile = “output_conv/convection_cylinder.h5”.format(step) +meshbox.save(savefile) +v_soln.save(savefile) +t_soln.save(savefile) +meshbox.generate_xdmf(savefile)

+
+
+

+
+if uw.mpi.size == 1:
+
+    import numpy as np
+    import pyvista as pv
+    import vtk
+
+    pv.global_theme.background = "white"
+    pv.global_theme.window_size = [750, 750]
+    pv.global_theme.antialiasing = True
+    pv.global_theme.jupyter_backend = "trame"
+    pv.global_theme.smooth_shading = True
+
+    pv.start_xvfb()
+
+    pvmesh = meshbox.mesh2pyvista(elementType=vtk.VTK_TRIANGLE)
+
+    points = np.zeros((t_soln.coords.shape[0], 3))
+    points[:, 0] = t_soln.coords[:, 0]
+    points[:, 1] = t_soln.coords[:, 1]
+
+    point_cloud = pv.PolyData(points)
+
+    with swarm.access():
+        points = np.zeros((swarm.data.shape[0], 3))
+        points[:, 0] = swarm.data[:, 0]
+        points[:, 1] = swarm.data[:, 1]
+
+    swarm_point_cloud = pv.PolyData(points)
+
+    with swarm.access():
+        swarm_point_cloud.point_data["T1"] = T1.data.copy()
+
+    with meshbox.access():
+        point_cloud.point_data["T"] = t_soln.data.copy()
+
+    with meshbox.access():
+        usol = stokes.u.data.copy()
+
+    pvmesh.point_data["T"] = uw.function.evaluate(t_soln.fn, meshbox.data)
+
+    arrow_loc = np.zeros((stokes.u.coords.shape[0], 3))
+    arrow_loc[:, 0:2] = stokes.u.coords[...]
+
+    arrow_length = np.zeros((stokes.u.coords.shape[0], 3))
+    arrow_length[:, 0:2] = usol[...]
+
+    pl = pv.Plotter()
+
+    pl.add_arrows(arrow_loc, arrow_length, mag=0.00002, opacity=0.75)
+    # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1)
+
+    # pl.add_points(point_cloud, cmap="coolwarm",
+    #               render_points_as_spheres=True,
+    #               point_size=7.5, opacity=0.25
+    #             )
+
+    pl.add_points(
+        swarm_point_cloud,
+        cmap="coolwarm",
+        render_points_as_spheres=True,
+        point_size=2.5,
+        opacity=0.5,
+        clim=[0.0, 1.0],
+    )
+
+    pl.add_mesh(
+        pvmesh,
+        cmap="coolwarm",
+        edge_color="Black",
+        show_edges=True,
+        scalars="T",
+        use_transparency=False,
+        opacity=0.5,
+        clim=[0.0, 1.0],
+    )
+
+    pl.show(cpos="xy")
+
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Utilities/Ex_UsingCoordinates.html b/main/Notebooks/Examples-Utilities/Ex_UsingCoordinates.html new file mode 100644 index 0000000..4b9a69e --- /dev/null +++ b/main/Notebooks/Examples-Utilities/Ex_UsingCoordinates.html @@ -0,0 +1,861 @@ + + + + + + + + + + + Symbolic forms v. mesh variables — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Symbolic forms v. mesh variables

+ +
+
+ +
+
+
+ + + + +
+ +
+
+
import petsc4py
+from petsc4py import PETSc
+
+import underworld3 as uw
+import numpy as np
+import sympy
+
+import os
+
+os.environ["SYMPY_USE_CACHE"] = "no"
+
+res = 0.1
+r_o = 1.0
+r_i = 0.5
+free_slip_upper = True
+
+
+
+
+
+
+
meshdisc_xyz = uw.meshing.Annulus(radiusOuter=r_o, radiusInner=r_i, cellSize=res)
+
+mesh = meshdisc_xyz
+PXs = sympy.Function(r"\mathcal{P}")(*mesh.N.base_scalars()[0 : mesh.dim])
+VXs = sympy.Matrix.zeros(1, mesh.dim)
+for i in range(mesh.dim):
+    VXs[i] = sympy.Function(rf"\mathcal{{V}}_{i}")(*mesh.N.base_scalars()[0 : mesh.dim])
+display(PXs)
+display(VXs)
+
+
+
+
+
+

Symbolic forms v. mesh variables#

+

Mesh variables are sympy.Function objects that have the additional property of carrying data that allows them to be interploated to any point on a mesh and numerically differentiated (once).

+

The symbolic forms allow us to undertake all the manipulations and simplifications available in sympy which are not all available for mesh variables (such as higher-order derivatives).

+

For example, we can demonstrate some vector calculus results which we can use while we develop our equation systems. We can substitute for the mesh variables later if we choose.

+
+
+
gradPXs = meshdisc_xyz.vector.gradient(PXs)
+display(gradPXs)
+
+divgradPXs = meshdisc_xyz.vector.divergence(gradPXs)
+display(divgradPXs)
+
+curlgradPXs = meshdisc_xyz.vector.curl(gradPXs)
+display(curlgradPXs)
+
+
+
+
+
+
+
## Create a Cylindrical Mesh with a Native Coordinate System
+
+meshball_xyz_tmp = uw.meshing.Annulus(radiusOuter=r_o, radiusInner=r_i, cellSize=res)
+
+xy_vec = meshball_xyz_tmp.dm.getCoordinates()
+xy = xy_vec.array.reshape(-1, 2)
+dmplex = meshball_xyz_tmp.dm.clone()
+rtheta = np.empty_like(xy)
+rtheta[:, 0] = np.sqrt(xy[:, 0] ** 2 + xy[:, 1] ** 2)
+rtheta[:, 1] = np.arctan2(xy[:, 1] + 1.0e-16, xy[:, 0] + 1.0e-16)
+rtheta_vec = xy_vec.copy()
+rtheta_vec.array[...] = rtheta.reshape(-1)[...]
+dmplex.setCoordinates(rtheta_vec)
+meshball_xyz_tmp.vtk("tmp_disk.vtk")
+del meshball_xyz_tmp
+
+meshdisc = uw.meshing.Mesh(
+    dmplex,
+    coordinate_system_type=uw.coordinates.CoordinateSystemType.CYLINDRICAL2D_NATIVE,
+)
+uw.cython.petsc_discretisation.petsc_dm_set_periodicity(
+    meshdisc.dm, [0.0, 1.0], [0.0, 0.0], [0.0, 2 * np.pi]
+)
+
+
+## Add some mesh variables (Vector and scalar)
+
+VC = uw.discretisation.MeshVariable(r"U^c", meshdisc, 2, degree=2)
+PC = uw.discretisation.MeshVariable(r"P^c", meshdisc, 1, degree=1)
+
+## Create some symbolic equivalents
+
+mesh = meshdisc
+PCs = sympy.Function(r"\mathcal{P}")(*mesh.N.base_scalars()[0 : mesh.dim])
+VCs = sympy.Matrix.zeros(1, mesh.dim)
+for i in range(mesh.dim):
+    VCs[i] = sympy.Function(rf"\mathcal{{V}}_{i}")(*mesh.N.base_scalars()[0 : mesh.dim])
+display(PCs)
+display(VCs)
+
+
+
+
+
+
+
gradPCs = meshdisc.vector.gradient(PCs)
+display(gradPCs)
+
+divgradPCs = meshdisc.vector.divergence(gradPCs)
+display(divgradPCs)
+
+curlgradPCs = meshdisc.vector.curl(gradPCs)
+display(curlgradPCs)
+
+
+
+
+
+
+
## Create a Spherical Mesh with a Native Coordinate System
+
+## NOTE: this only works if numElements is an odd number
+
+meshball_xyz_tmp = uw.meshing.CubedSphere(
+    radiusOuter=r_o, radiusInner=r_i, numElements=7, simplex=True
+)
+
+xyz_vec = meshball_xyz_tmp.dm.getCoordinates()
+xyz = xyz_vec.array.reshape(-1, 3)
+dmplex = meshball_xyz_tmp.dm.clone()
+
+rl1l2 = np.empty_like(xyz)
+rl1l2[:, 0] = np.sqrt(xyz[:, 0] ** 2 + xyz[:, 1] ** 2 + xyz[:, 2] ** 2)
+rl1l2[:, 1] = np.arctan2(xyz[:, 1], xyz[:, 0])
+rl1l2[:, 2] = (
+    np.arctan2(np.sqrt(xyz[:, 0] ** 2 + xyz[:, 1] ** 2), xyz[:, 2]) - np.pi / 2
+)
+
+rl1l2_vec = xyz_vec.copy()
+rl1l2_vec.array[...] = rl1l2.reshape(-1)[...]
+dmplex.setCoordinates(rl1l2_vec)
+
+meshball_xyz_tmp.vtk("tmp_sphere.vtk")
+del meshball_xyz_tmp
+
+meshball = uw.meshing.Mesh(
+    dmplex, coordinate_system_type=uw.coordinates.CoordinateSystemType.SPHERICAL_NATIVE
+)
+uw.cython.petsc_discretisation.petsc_dm_set_periodicity(
+    meshball.dm, [0.0, 6.28, 0.0], [0.0, 0.0, 0.0], [0.0, 2 * np.pi, 0.0]
+)
+
+
+## Add some mesh variables (Vector and scalar)
+
+VS = uw.discretisation.MeshVariable(r"U^s", meshball, 3, degree=2)
+PS = uw.discretisation.MeshVariable(r"P^s", meshball, 1, degree=1)
+
+## Create some symbolic equivalents
+
+mesh = meshball
+PSs = sympy.Function(r"\mathcal{P}")(*mesh.N.base_scalars())
+VSs = sympy.Matrix.zeros(1, mesh.dim)
+for i in range(mesh.dim):
+    VSs[i] = sympy.Function(rf"\mathcal{{V}}_{i}")(*mesh.N.base_scalars())
+display(PSs)
+display(VSs)
+
+
+
+
+
+
+
gradPSs = meshball.vector.gradient(PSs)
+display(gradPSs)
+
+divVSs = meshball.vector.divergence(VSs)
+display(divVSs)
+
+divgradPSs = meshball.vector.divergence(gradPSs)
+display(divgradPSs)
+
+curlgradPSs = meshball.vector.curl(gradPSs)
+display(curlgradPSs)
+
+# Note
+sympy.simplify(curlgradPSs)
+
+
+
+
+
+
+
# if uw.mpi.size == 1:
+
+#     import numpy as np
+#     import pyvista as pv
+#     import vtk
+
+#     pv.global_theme.background = "white"
+#     pv.global_theme.window_size = [1000, 1000]
+#     pv.global_theme.antialiasing = True
+#     pv.global_theme.jupyter_backend = "panel"
+#     pv.global_theme.smooth_shading = True
+
+#     pvmesh = pv.read("tmp_sphere.vtk")
+
+#     pl = pv.Plotter(window_size=(750, 750))
+
+#     pl.add_mesh(pvmesh,'Black', 'wireframe')
+
+#     pl.show(cpos="xy")
+
+
+
+
+
+
+
display(meshball.CoordinateSystem.rRotN)
+display(meshball.CoordinateSystem.xRotN)
+
+
+
+
+
+
+
# We can validate using the pure symbolic forms
+
+gradPSs = meshball.vector.gradient(PSs)
+divVSs = meshball.vector.divergence(VSs)
+curlVSs = meshball.vector.curl(VSs)
+
+sympy.simplify(meshball.vector.divergence(curlVSs))
+
+
+
+
+
+
+
sympy.simplify(meshball.vector.gradient(divVSs))
+
+
+
+
+
+
+
gradPSs
+
+
+
+
+
+
+
divVSs
+
+
+
+
+
+
+
gradPs = meshball.vector.gradient(PS.sym)
+gradPs
+
+
+
+
+
+
+
divVs = meshball.vector.divergence(VS.sym)
+divVs
+
+
+
+
+
+
+
curlVs = meshball.vector.curl(VS.sym)
+curlVs
+
+
+
+
+
+
+
meshball.vector.strain_tensor(VS.sym)
+
+
+
+
+
+
+
PS.sym.diff(meshball.CoordinateSystem.N[0])
+
+
+
+
+
+
+
PS.sym
+
+
+
+
+
+
+
sympy.simplify(meshball.vector.divergence(meshball.vector.gradient(P)))
+
+
+
+
+
+
+
meshball.N.base_vectors()
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Utilities/Ex_scaling.html b/main/Notebooks/Examples-Utilities/Ex_scaling.html new file mode 100644 index 0000000..9d787a5 --- /dev/null +++ b/main/Notebooks/Examples-Utilities/Ex_scaling.html @@ -0,0 +1,805 @@ + + + + + + + + + + + Underworld scaling example — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Underworld scaling example

+ +
+ +
+
+ + + + +
+ +
+

Underworld scaling example#

+

How to utilise the scaling functionality that is included with UW to easily convert between dimensional and non-dimensional values

+
+
+
import numpy as np
+import underworld3 as uw
+
+
+
+
+
+
+
# import unit registry to make it easy to convert between units
+u = uw.scaling.units
+
+### make scaling easier
+ndim = uw.scaling.non_dimensionalise
+dim  = uw.scaling.dimensionalise 
+
+
+
+
+
+
+
### reference values
+length         = 100.                      #* u.kilometer
+kappa          = 1e-6                      #* u.meter**2/u.second
+g              = 9.81                      #* u.meter/u.second**2
+v              = 1                         # u.centimeter/u.year, velocity in cm/yr
+alpha          = 3.0e-5                    # *1./u.kelvin
+tempMax        = 1573.15                   # * u.kelvin
+tempMin        = 273.15                    #* u.kelvin
+rho0           = 3300.0                    #* u.kilogram / u.metre**3#  * 9.81 * u.meter / u.second**2
+R              = 8.3145                    # [J/(K.mol)], gas constant
+
+
+
+
+

Create the fundamental values required to obtain scaling for all units

+
+
+
lengthScale   = length  * u.kilometer
+surfaceTemp   = tempMin * u.degK
+baseModelTemp = tempMax * u.degK
+bodyforce     = rho0    * u.kilogram / u.metre**3 * g * u.meter / u.second**2
+
+half_rate     = v * u.centimeter / u.year
+
+KL = lengthScale.to_base_units()
+Kt = (KL / half_rate).to_base_units()
+KM = (bodyforce * KL**2 * Kt**2).to_base_units()
+KT = (baseModelTemp - surfaceTemp).to_base_units()
+
+
+
+
+
+
+
scaling_coefficients                  = uw.scaling.get_coefficients()
+
+scaling_coefficients["[length]"]      = KL.to_base_units()
+scaling_coefficients["[time]"]        = Kt.to_base_units()
+scaling_coefficients["[mass]"]        = KM.to_base_units()
+scaling_coefficients["[temperature]"] = KT.to_base_units()
+
+
+scaling_coefficients
+
+
+
+
+
+
+
### fundamental values
+ref_length    = uw.scaling.dimensionalise(1., u.meter).magnitude
+
+ref_length_km = uw.scaling.dimensionalise(1., u.kilometer).magnitude
+
+ref_density   =  uw.scaling.dimensionalise(1., u.kilogram/u.meter**3).magnitude
+
+ref_gravity   = uw.scaling.dimensionalise(1., u.meter/u.second**2).magnitude
+
+ref_temp      = uw.scaling.dimensionalise(1., u.kelvin).magnitude
+
+ref_velocity  = uw.scaling.dimensionalise(1., u.meter/u.second).magnitude
+
+### derived values
+ref_time      = ref_length / ref_velocity
+
+ref_pressure  = ref_density * ref_gravity * ref_length
+
+ref_stress    = ref_pressure
+
+ref_viscosity = ref_pressure * ref_time
+
+### Key ND values
+ND_diffusivity = kappa        / (ref_length**2/ref_time)
+ND_gravity     = g            / ref_gravity
+
+
+
+
+
+
+
if uw.mpi.rank == 0:
+    print(f'time scaling: {ref_time/(60*60*24*365.25*1e6)} [Myr]')
+    print(f'pressure scaling: {ref_pressure/1e6} [MPa]')
+    print(f'viscosity scaling: {ref_viscosity} [Pa s]')
+    print(f'velocity scaling: {ref_velocity*(1e2*60*60*24*365.25)} [cm/yr]')
+    print(f'length scaling: {ref_length/1e3} [km]')
+
+
+
+
+
+

How to non-dimensionalise a value#

+
+
+
cohesion = 10e6
+
+
+
+
+
+
+
### first way, using the UW non-dimensionalise function, have to provide the units
+nd_cohesion = ndim(cohesion * u.pascal)
+nd_cohesion
+
+
+
+
+
+
+
#### second way, just divide cohesion by the scaled reference value
+cohesion/ref_stress
+
+
+
+
+
+
+
#### check if they are close
+np.isclose(ndim(cohesion * u.pascal), (cohesion/ref_pressure), rtol=1e-10, atol=1e-10)
+
+
+
+
+
+
+
viscosity = 1e21
+
+
+
+
+
+
+
nd_visc = ndim(viscosity * u.pascal*u.second)
+
+nd_visc
+
+
+
+
+
+
+
viscosity / ref_viscosity
+
+
+
+
+
+
+
np.isclose(ndim(viscosity * u.pascal*u.second), (viscosity / ref_viscosity), rtol=1e-10, atol=1e-10)
+
+
+
+
+
+
+
+

How to dimensionalise#

+
+
+
### can either use the inbuilt function, where you provide the units
+
+dim(nd_visc, u.pascal*u.second)
+
+
+
+
+
+
+
#### or multiply by the ref value which has units of Pa s
+nd_visc * ref_viscosity
+
+
+
+
+
+
+
### you can remove the units by using the magnitude function 
+print(dim(nd_visc, u.pascal*u.second).magnitude)
+
+#### or use .m (short for .magnitude)
+print(dim(nd_visc, u.pascal*u.second).m)
+
+
+
+
+
+
+
#### check if they give the same answers
+np.isclose(dim(nd_visc, u.pascal*u.second).m, nd_visc * ref_viscosity,  rtol=1e-10, atol=1e-10)
+
+
+
+
+
+

check mesh and swarm vars work#

+

Currently not supported

+
+
+
# Set the resolution.
+res = 32
+xmin, xmax = 0, 1
+ymin, ymax = 0, 1
+
+mesh = uw.meshing.StructuredQuadBox(elementRes=(int(res), int(res)), minCoords=(xmin, ymin), maxCoords=(xmax, ymax))
+
+
+
+
+
+
+
+
dim(mesh.data, u.kilometer)
+
+
+
+
+
+
+
mesh.data
+
+
+
+
+
+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Utilities/Readme.html b/main/Notebooks/Examples-Utilities/Readme.html new file mode 100644 index 0000000..a86400d --- /dev/null +++ b/main/Notebooks/Examples-Utilities/Readme.html @@ -0,0 +1,586 @@ + + + + + + + + + + + Miscellaneous Tips and Tricks — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Miscellaneous Tips and Tricks

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

Miscellaneous Tips and Tricks#

+
+

Recent solver and visualization updates#

+
    +
  • [x] Ex_Integrals_on_Meshes.py

    +
      +
    • [ ] RuntimeError: The mesh requires at least a single variable for integration to function correctly. This is a PETSc limitation. Create a ticket.

    • +
    +
  • +
  • [ ] Ex_Tensor_Variables_etc.py

    +
      +
    • [ ] Temperature mesh variable not defined and require changes in advDiff system

    • +
    +
  • +
  • [x] Ex_Projection_Function_Eval.py

  • +
  • [x] Ex_UsingCoordinates.py

  • +
  • [x] Ex_scaling.py

  • +
  • [x] Ex_Anisotropy.py

  • +
  • [x] Ex_BCs_from_meshVariables.py

  • +
  • [ ] Ex_ConstitutiveTensors.py

    +
      +
    • [ ] AttributeError: type object ‘TransverseIsotropicFlowModel’ has no attribute ‘Parameters’

    • +
    +
  • +
+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Examples-Utilities/output/README.html b/main/Notebooks/Examples-Utilities/output/README.html new file mode 100644 index 0000000..1642c7a --- /dev/null +++ b/main/Notebooks/Examples-Utilities/output/README.html @@ -0,0 +1,520 @@ + + + + + + + + + + + Utilities — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Utilities

+ +
+
+ +
+
+
+ + + + +
+ +
+

Utilities#

+

model outputs

+

These files are NOT under version control

+
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Notebooks/Index.html b/main/Notebooks/Index.html new file mode 100644 index 0000000..5b83fdd --- /dev/null +++ b/main/Notebooks/Index.html @@ -0,0 +1,596 @@ + + + + + + + + + + + Index of Sample notebooks — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Index of Sample notebooks

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

Index of Sample notebooks#

+

Typically, a markdown (myst format) file is used for text that is not going to be executed as a notebook. Notebooks are organised into a number of folders:

+
    +
  • Examples-Convection

  • +
  • Examples-Meshing

  • +
  • Examples-NavierStokes

  • +
  • Examples-PoissonEquation

  • +
  • Examples-PorousFlow

  • +
  • Examples-Sandbox-VEP

  • +
  • Examples-StokesFlow

  • +
  • Examples-UW-2to3

  • +
  • Examples-Utilities

  • +
+ +
+

Python packages#

+

Any packages that you need to have installed by conda or pip should be included in the conda_packages.yml file in the root directory of the repository. You can also specify packages for the apt get system if they cannot be installed by conda.

+

+
+
+
+
+

+
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Theory/FiniteElements.html b/main/Theory/FiniteElements.html new file mode 100644 index 0000000..54841ed --- /dev/null +++ b/main/Theory/FiniteElements.html @@ -0,0 +1,541 @@ + + + + + + + + + + + The Lagrangian-Particle FEM — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

The Lagrangian-Particle FEM

+ +
+
+ +
+
+
+ + + + +
+ +
+

The Lagrangian-Particle FEM#

+

Motivation

+

Relation to classical FEM, classical FEM derivations

+

Relation to MPM etc

+

(Grab notes from numerical methods “book”)

+

Details of implementation

+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Theory/Index.html b/main/Theory/Index.html new file mode 100644 index 0000000..175d6cd --- /dev/null +++ b/main/Theory/Index.html @@ -0,0 +1,540 @@ + + + + + + + + + + + Underworld Theory Manual — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Underworld Theory Manual

+ +
+
+ +
+
+
+ + + + +
+ +
+

Underworld Theory Manual#

+

Background theory, in common with Underworld publications and with full references.

+

Not sure whether to put comp. sci. theory in here like how the functions work … maybe in their own section ?

+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Theory/LinearAlgebra.html b/main/Theory/LinearAlgebra.html new file mode 100644 index 0000000..af58a03 --- /dev/null +++ b/main/Theory/LinearAlgebra.html @@ -0,0 +1,537 @@ + + + + + + + + + + + Linear Algebra — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Linear Algebra

+ +
+
+ +
+
+
+ + + + +
+ +
+

Linear Algebra#

+

The realisation of the Finite Elements and the Mathematical models in the form of matrices, combined with our strategies for solving them.

+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Theory/MathematicalBackground.html b/main/Theory/MathematicalBackground.html new file mode 100644 index 0000000..dbff49d --- /dev/null +++ b/main/Theory/MathematicalBackground.html @@ -0,0 +1,539 @@ + + + + + + + + + + + Mathematical Background — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Mathematical Background

+ +
+
+ +
+
+
+ + + + +
+ +
+

Mathematical Background#

+

Equation systems

+

Constitutive laws

+

Elasticity, Plasticity formulation

+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Theory/NonLinear.html b/main/Theory/NonLinear.html new file mode 100644 index 0000000..5d93af7 --- /dev/null +++ b/main/Theory/NonLinear.html @@ -0,0 +1,538 @@ + + + + + + + + + + + Non Linear Methods — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Non Linear Methods

+ +
+
+ +
+
+
+ + + + +
+ +
+

Non Linear Methods#

+

TBA

+

SNES stuff would be good here.

+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/Theory/PointwiseFunctions.html b/main/Theory/PointwiseFunctions.html new file mode 100644 index 0000000..620b183 --- /dev/null +++ b/main/Theory/PointwiseFunctions.html @@ -0,0 +1,956 @@ + + + + + + + + + + + PETSc pointwise functions and PDE solvers — Underworld 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + +
+ +
+

PETSc pointwise functions and PDE solvers#

+

As we saw in [the Finite Element Pages], the finite element method provides a very general way to approach the numerical solution of a very wide variety of problems in continuum mechanics using a standardised, matrix-based formulation and with considerable flexibility in the choice of discretisation, mesh geometry, and the ability to deal very naturally with jumps in material properties.

+

However, the FEM is based upon a variational, “weak” form of the governing equations of any problem while most publications outline the strong form of any problem and this can make it difficult for anyone without a strong mathematical background to translate quickly from publication to a new model.

+

PETSc provides a mechanism to automatically generate a finite element weak form from the strong (point-wise) form of the governing equations. This takes the form of a template equation and, in the case of non-linear problems, a template for the corresponding Jacobian derivatives.

+

The PETSc strong-form interface asks the user to provide pre-compiled functions with a pre-defined pattern of arguments relating known nodal-point variables, shape-functions and derivatives at any point in the domain. If the strong form is written

+
+\[ \mathbf{F}(\mathbf{u}) \sim \nabla.\mathbf{f}_1(u, \nabla u) - f_0 (u, \nabla u) = 0 \]
+

Where the \(f_0\) term, generally speaking, represents the forces and \(f_1\) comprises flux terms. +Then a corresponding weak form is

+
+\[ \phi^T \mathbf{F}(\mathbf{u}) \sim \int_\Omega \phi \cdot f_0 \left(u, \nabla u \right) + + \nabla \phi \mathbf{f}_1 \left(u, \nabla u \right) = 0 \]
+

The discrete form of this equation has some obvious similarities to the standard finite element matrix form except that the functions \(f_0\) and \(\mathbf{f}_1\) are not directly expressed in terms of the basis function matrices:

+
+\[ \cal{F}(\cal{u}) \sim \sum_e \epsilon_e^T \left[ B^T W f_0(u^q, \nabla u^q) + + \sum_k D_k^T W \mathbf{f}_1^k (u^q, \nabla u^q) \right] = 0 +\]
+

where \(q\) represents a set of quadrature points, \(W\) is a matrix of weights and \(B\), \(D\) are the usual basis function matrices but here evaluated at the quadrature points. \(\epsilon\) restricts the terms to the element. See [KBRS13] for details.

+

The user must provide a compiled representation of the terms \(f_0\) and \(\mathbf{f}_1\) but it is also necessary to provide the corresponding compiled representations of the Jacobians which satisfy

+
+\[\begin{split} \cal{F'}(\cal{u}) \sim \sum_e \epsilon_e^T + \left[ \begin{array}{cc} + B^T & \mathbf{D}^T \\ + \end{array} \right] + \mathbf{W} + \left[ \begin{array}{cc} + f_{0,0} & f_{0,1} \\ + \mathbf{f}_{1,0} & \mathbf{f}_{1,1} \\ + \end{array}\right] + \left[ \begin{array}{c} + B^T \\ + \mathbf{D}^T + \end{array} \right] \epsilon_e +\quad \mathrm{and} \quad + f_{[i,j]} = + \left[ \begin{array}{cc} + \partial f_0 / \partial u & \partial f_0 / \partial \nabla u \\ + \partial \mathbf{f}_1 / \partial u & \partial \mathbf{f}_1 / \partial \nabla u + \end{array}\right] +\end{split}\]
+

This formulation greatly simplifies writing optimal code as the user sets up a problem without touching the bulk of the implementation. The only difficulty is ensuring that the Jacobians are derived consistently and correctly. As the Jacobians need to be calculated and implemented as separate functions this introduces a potential point of failure.

+

In underworld, we provide a fully symbolic approach to specifying the strong form terms, \(f_0\) and \(\mathbf{f}_1\) using the sympy symbolic algebra python package. sympy is also able to differentiate +\(f_0\) and \(\mathbf{f}_1\) to obtain the relevant Jacobian matrices in symbolic form. We also take +advantage of sympys automatic code generation capabilities to compile functions that match +the PETSc templates.

+

This provides a very natural mapping between the formulation above and the sympy python code.

+

+        # X and U are sympy position vector, unknown vector 
+        # that may include mesh variables 
+ 
+        U_grad = sympy.derive_by_array(U, X)
+
+        F0 = f
+        F1 = kappa * U_grad
+
+        # Jacobians are 
+
+        J_00 = sympy.derive_by_array(F0, U)
+        J_01 = sympy.derive_by_array(F0, U_grad)
+        J_10 = sympy.derive_by_array(F1, U)
+        J_11 = sympy.derive_by_array(F1, U_grad)
+
+        # Pass F0, F1, J_xx to sympy code generation routines
+
+        # ...
+
+
+
+

Note: some re-indexing may be necessary to interface between sympy and PETSc +especially for vector problems.

+

Note: underworld provides a translation between mesh variables and their sympy +symbolic representation on the user-facing side that also needs to translate +to PETSc data structures in the compiled code.

+
+

Underworld Solver Classes#

+

We provide 3 base classes to build solvers. These are a scalar SNES solver, +a vector SNES solver and a Vector SNES saddle point solver (constrained vector problem). +These are bare-bones classes that implement the pointwise function / sympy approach that +can then be used to build solvers for many common situations.

+

A blank slate is a very scary thing and so we provide templates for some common equations +and examples to show how these can be extended.

+
+
+

Implementation & Examples#

+
+

Poisson Solvers#

+

(link to another document) +Diffusion

+

Darcy flow

+

Advection-diffusion (SLCN)

+
+
+

Advection dominated flow#

+

(link to another document)

+

Swarm-based problems

+

Projection / swarm evaluation

+

Advection-diffusion (Swarm)

+

Material point methods

+
+
+

Incompressible Stokes#

+

(link to another document)

+

Saddle point problems

+

Stokes, boundary conditions, constraints

+

Navier-Stokes (Swarm)

+

Viscoelasticity

+
+
+
+

Remarks#

+

The generic solver classes can be used to construct all of the examples above. The equation-system classes that we provide help to provide a template or scaffolding for a less experienced user and they also help to orchestrate cases where multiple solvers come together in a specific order (e.g. the Navier-Stokes case where history variable +projections need to be evaluated during the solve).

+

Creating sub-classes from the equation systems or the generic solvers is an excellent way to build workflows whenever there is a risk of exposing some fragile construction at the user level.

+

Some of the need for these templates is a result of inconsistencies in the way sympy treats matrices, vectors and tensor (array) objects. We expect this to change over time.

+
+
+

Example 1 - The Poisson Equation#

+

The classical form of the scalar Poisson Equation is

+
+\[ \alpha \nabla^2 \psi = f \]
+

Where \(\psi\) is an unknown scalar quantity, \(\alpha\) is +a constitutive parameter that relates gradients to fluxes, and \(f\) +is a source term.

+

This equation is obtained by considering the divergence of fluxes needed to +balance the sources. For example, in thermal diffusion we identify +\(\psi\) with the temperature, \(T\), and the constitutive parameter, \(k\), +is a thermal conductivity.

+
+\[ \nabla \cdot k \nabla T = h \]
+

In this form, \(\mathbf{q} = k \nabla T\) is Fourier’s expression of the +heat flux in terms of temperature gradients.This form matches the template above if we identify:

+
+\[ f_0 = -h \quad \textrm{and} \quad f_1 = k\nabla T\]
+

and, in fact, this is exactly what we need to specify in the underworld equation +system.

+
        solver.L= sympy.derive_by_array(solver.U, solver.X).transpose()
+
+        # f0 residual term (weighted integration) - scalar function
+        solver.F0 = -h
+
+        # f1 residual term (integration by parts / gradients)
+        solver.F1 = k * solver.L
+
+
+

which means the user only needs to supply a mesh, a mesh variable to +hold the solution and sympy expressions for \(k\) and \(h\) in order +to solve a Poisson equation.

+

The SNES_Poisson class is a very lightweight wrapper on +the SNES_Scalar class which provides a template for the flux +term and very little else. +\(F_0\) and \(F_1\) are inherited as an empty scalar and vector respectively. +These are available in the template for the user to extend the equation as needed.

+

This notebook compares +the generic class and the one with the flux templated.

+
+
+

Example 2 - Projections and Evaluations#

+

PETSc has a very general concept of discretisation spaces that do not +necessarily admit to continuous interpolation to or from arbitrary points. +For this reason, a more general concept is to create projections that map +between representations of the data. For example, in Finite Elements, +fluxes are generally not available at nodal points because shape functions +have discontinuous gradients there. To compute fluxes at nodal points, we +would establish a projection problem to form a best fitting continous function +to the values at points where we can evaluate the fluxes. In addition, +sympy functions (including those for fluxes) that contain derivatives of finite element variables +can not be evaluated numerically by sympy but can be evaluated as compiled +functions in the context of a solver.

+

We write these evaluations using the Projection solver classes. This is +the simplest of the solvers and we are only discussing it second because it +is almost too simple to be instructive (and because the weak form of this +equation is the natural one to work with).

+

We would like to solve for a continuous, nodal point solution \(u\) that +satisfies as best possible,

+
+\[ \int_\Omega \phi u d\Omega = \int_\Omega \phi \tilde{u} d\Omega \]
+

where \(\tilde{u}\) is a function with unknown continuity that we +are able to evaluate at integration points in the mesh.

+

The generic solver specification in underworld looks like this

+

+        # f0 residual term (weighted integration) - scalar function
+        solver.F0 = solver.u.fn - user_uw_function
+
+        # f1 residual term (integration by parts / gradients)
+        solver.F1 = 0.0
+
+
+

where user_uw_function is some sympy expression in terms of spatial +coordinates that can include mesh or swarm variables. solver.u.fn is the +mesh variable (in function form) where the solution will reside.

+

In principle we could add a smoothing term via solver.F1 and, importantly, +we can also add boundary conditions or constraints that need to be satisfied in +addition to fitting the integration point values.

+

We provide projection operators for scalar fields, vector fields and +solenoidal vector fields (ensuring that the projection remains divergence free). These +provide templates for the \(F_0\) and \(F_1\) terms with generic smoothing. +For an explanation of the divergence free projection methodology, see the next +example on the incompressible Stokes problem.

+

This notebook has an example of +each of these cases.

+
+
+

Example 3 - Incompressible Stokes Equation#

+

The incompressible Stokes Equation is an example of a problem with an +additional constraint equation that must be satisfied by the solution. +Variational formulations (the weak form of +classical finite elements is one) naturally lend themselves +to the addition of multiple constraints into the functional to be minimised.

+

Unfortunately, the incompressiblity constraint needs to be enforced very strongly +to obtain reasonable solutions and this can lead to unacceptably +ill conditioned systems that are slow or impossible to solve.

+

Alternatively, we solve a coupled problem in which additional, kinematic, +parameters of the constraint term are introduced. This forms a new block system of +equations that is a general expression for a large range of constrained problems.

+

These are often known as saddle point problems which represents the trade-off +between satisfying the original equations and the constraint equations. +(Saddle point here refers to the curvature of the functional we are +optimising) See: M. Benzi, G. Golub and J. Liesen, +Numerical solution of saddle point problems, Acta Numerica 14 (2005), +pp. 1–137. for a general discussion.

+

The coupled equation system we want to solve is

+
+\[ \nabla \cdot \mathbf{\tau} - \nabla p = f_\textrm{buoy} \]
+

with the constraint

+
+\[ \nabla \cdot \mathbf{u} = 0 \]
+

The saddle-point solver requires us to specify both of these equations and +to provide two solution vectors \(\mathbf{u}\) and \(\mathbf{p}\). In this +system, \(\mathbf{p}\) is the parameter that enforces the incompressiblity +constraint equation and is physically identifiable as a pressure.

+

+        # definitions 
+
+        U_grad = sympy.derive_by_array(solver.U, solver.X)
+
+        strainrate = (sympy.Matrix(U_grad) + sympy.Matrix(U_grad).T)/2
+        stress     = 2*solver.viscosity*solver.strainrate
+
+        # set up equation terms
+
+        # u f0 residual term (weighted integration) - vector function
+        solver.UF0 = - solver.bodyforce
+
+        # u f1 residual term (integration by parts / gradients) - tensor (sympy.array) term
+        solver.UF1 = stress
+
+        # p f0 residual term (these are the constraints) - vector function
+
+        solver.PF0 = sympy.vector.divergence(solver.U)
+
+
+

In underworld, the SNES_Stokes solver class is responsible for managing the +user interface to the saddle point system for incompressible Stokes flow.

+
+
+

Example 4 - Advection in the absence of diffusion#

+

The pure transport equation can be written in Lagrangian

+
+\[ \frac{D \psi}{D t} = 0 \]
+

or in Eulerian form

+
+\[ \frac{\partial \psi}{\partial t} + \mathbf{u} \cdot \nabla \psi = 0 \]
+

In the Lagrangian form, there is nothing to solve, provided the fluid-transported reference frame is available. In the Eulerian form, the non-linear advection term +\(\mathbf{u} \cdot \nabla \psi\) is reknowned for being difficult to solve, especially in the pure-transport form.

+

Underworld provides discrete Lagrangian swarm variables [ CROSSREF ] that make it straightforward to work with transported quantities +on a collection of moving sample points that we normally refer to as particles. +Behind the scenes, there is complexity in 1) following the Lagrangian reference frame accurately, +2) mapping the fluid-deformed reference frame to the stationary mesh, and 3) for +parallel meshes, migrating particles (and their data) across the decomposed domain.

+

The swarm that manages the variables is able to update the locations of the particles +when provided with a velocity vector field and a time increment and will handle the +particle re-distribution in the process.

+

Each variable on a swarm has a corresponding mesh variable (a proxy variable) that +is automatically updated when the particle locations change. The proxy variable is +computed through a projection (see above).

+

Note: If specific boundary conditions need to be applied, it is necessary for the user +to define their own projection operator, apply the boundary conditions, and solve when needed. +(Feature request: allow user control over the projection, including +boundary conditions / constraints, so that this is not part of the user’s responsibility)

+
+
+

Example 5 - The Scalar Advection-diffusion Equation#

+

The situation where a quantity is diffusing through a moving fluid.

+
+\[ \frac{\partial \psi}{\partial t} + \mathbf{u}\cdot\nabla\psi = \nabla \cdot \alpha \nabla \psi + f\]
+

where \(\mathbf{u}\) is a (velocity) vector that transports \(\psi\) and \(\alpha\) is a +diffusivity. In Lagrangian form (following \(\mathbf{u}\)),

+
+\[ \frac{D \psi}{D t} = \nabla \cdot \alpha \nabla \psi + f\]
+

As before, the advection terms are greatly simplified in a Lagrangian reference +frame but now we also have diffusion terms and boundary conditions that are easy +to solve accurately in an Eulerian mesh but which must also be applied to variables +that derive from a Lagrangian swarm (which has no boundary conditions of its own).

+

Advection-diffusion equations are often dominated by the presence of boundary layers where +advection of a quantity (along the direction of flow) is balanced by a diffusive flux +in the cross-stream direction. Under these conditions, there is some work to be done to +ensure that these two terms are calculated consistently and this is particularly important +close to regions where boundary conditions need to be applied.

+

The approach in underworld is to provide a solver structure to manage +advection-diffusion problems on behalf of the user. We use +a swarm-variable for tracking the history of the \(\psi\) as it is transported +by \(\mathbf{u}\) and we allow the user to specify (solve for) this flow, and +to update the swarm positions accordingly. The history variable, \(\psi^*\) +is the value of \(\psi\) upstream at an earlier timestep and allows us to +approximate \(D \psi/Dt\) as a finite difference approximation along the +characteristics of the advection operator:

+
+\[\left. \frac{D \psi}{Dt} \right|_{p} \approx \frac{\psi_p - \psi^*_p}{\Delta t}\]
+

Here, the subscript \(p\) indicates a value at a particle in the Lagrangian swarm.

+

This approach leads to a very natural problem description in python that corresponds closely to the mathematical formulation, namely:

+
        solver.L     = sympy.derive_by_array(solver.U,      solver.X).transpose()
+        solver.Lstar = sympy.derive_by_array(solver.U_star, solver.X).transpose()
+
+        # f0 residual term
+        solver._f0 = -solver.f + (solver.U.fn - solver.U_star.fn) / solver.delta_t
+
+        # f1 residual term (backward Euler)  
+        solver._f1 =  solver.L * solver.k
+
+        ## OR 
+
+        # f1 residual term (Crank-Nicholson)
+        solver._f1 =  0.5 * (solver.L + solver.Lstar) * solver.k
+
+
+

In the above, the U_star variable is a projection of the Lagrangian history variable +\(\psi^*_p\) onto the mesh subject to the same boundary conditions as \(\psi\).

+

In the SNES_AdvectionDiffusion_Swarm class (which is derived from SNES_Poisson), +the solve method solves for U_star using an in-built projection and boundary +conditions copied from the parent, before calling a standard Poisson solver. This class manages every aspect of the creation, refresh and solution of the necessary +projection subroutines Lagrangian history term, but not the update of this variable or the advection.

+

Caveat emptor: In the Crank-Nicholson stiffness matrix terms above, we form the derivatives in both the flux and the flux history with the same operator where, strictly, we should transport the derivatives (or form derivatives with respect to the transported coordinate system).

+
+
+

Example 6 - Navier-Stokes#

+

The incompressible Navier-Stokes equation of fluid dynamics is essentially the vector equivalent of the +scalar advection-diffusion equation above, in which the transported quantity is the velocity (strictly momentum) vector that is also responsible for the transport.

+
+\[ \rho \frac{\partial \mathbf{u}}{\partial t} + \mathbf{u}\cdot\nabla\mathbf{u} = \nabla \cdot \eta \left( \nabla \mathbf{u} + \nabla \mathbf{u}^T \right)/2 + \rho \mathbf{g}\]
+
+\[ \nabla \cdot \mathbf{u} = 0 \]
+

Obviously this is a strongly non-linear problem, but simply introduce the time dependence to the Stokes equation in the same way as we did for the Poisson equation above. A finite difference representation of the Lagrangian derivative of the velocity is defined using a vector swarm variable

+
+\[\left. \frac{D \mathbf{u}}{Dt} \right|_{p} \approx \frac{\mathbf{u}_p - \mathbf{u}^*_p}{\Delta t}\]
+

And the python problem description becomes:

+
        # definitions 
+
+        U_grad      = sympy.derive_by_array(solver.U,     solver.X)
+        U_grad_star = sympy.derive_by_array(solver.Ustar, solver.X)
+
+        strainrate = (sympy.Matrix(U_grad) + sympy.Matrix(U_grad).T)/2
+        stress     = 2*solver.viscosity*solver.strainrate
+
+        strainrate_star = (sympy.Matrix(U_grad_star) + sympy.Matrix(U_grad_star).T)/2
+        stress_star     = 2*solver.viscosity*solver.strainrate_star
+
+        # set up equation terms
+
+        # u f0 residual term (weighted integration) - vector function
+        solver.UF0 = - solver.bodyforce + solver.rho * (solver.U.fn - solver.U_star.fn) / solver.delta_t
+
+        # u f1 residual term (integration by parts / gradients) - tensor (sympy.array) term
+        solver.UF1 = 0.5 * stress * 0.5 * stress_star
+
+        # p f0 residual term (these are the constraints) - vector function
+        solver.PF0 = sympy.vector.divergence(solver.U)
+
+
+

Note, again, that, formulated in this way, the stress and strain-rate history variables neglect terms resulting from the deformation of the coordinate system over the timestep, \(\Delta t\). We could instead transport the strain rate or stress

+
+
+ + + + +
+ + + + + + + + +
+ + + + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/main/_images/AdvectionTestFigure.png b/main/_images/AdvectionTestFigure.png new file mode 100644 index 0000000..76c7d35 Binary files /dev/null and b/main/_images/AdvectionTestFigure.png differ diff --git a/main/_images/SinkerSolution.png b/main/_images/SinkerSolution.png new file mode 100644 index 0000000..8392aca Binary files /dev/null and b/main/_images/SinkerSolution.png differ diff --git a/main/_images/SocialShare.png b/main/_images/SocialShare.png new file mode 100644 index 0000000..713dab6 Binary files /dev/null and b/main/_images/SocialShare.png differ diff --git a/main/_sources/Benchmarking/DiffusionEquations.md b/main/_sources/Benchmarking/DiffusionEquations.md new file mode 100644 index 0000000..e69de29 diff --git a/main/_sources/Benchmarking/NavierStokes.md b/main/_sources/Benchmarking/NavierStokes.md new file mode 100644 index 0000000..529998e --- /dev/null +++ b/main/_sources/Benchmarking/NavierStokes.md @@ -0,0 +1,17 @@ +# Navier-Stokes Benchmarks + +## Steady solution + +This benchmark is described here: + + +## Periodic solution (Vortex shedding) + +This benchmark is described here: + + +![NS Vortex shedding benchmark](../Figures/Diagrams/NS_Benchmark.png) + + +Standard one used in the getting started section of `Fenics` + diff --git a/main/_sources/Benchmarking/Stokes.md b/main/_sources/Benchmarking/Stokes.md new file mode 100644 index 0000000..9f72244 --- /dev/null +++ b/main/_sources/Benchmarking/Stokes.md @@ -0,0 +1,10 @@ +# Stokes Flow + +# Analytic benchmarking + +# Symmetry testing + +(e.g. mode coupling etc where we know which modes should be excluded) + +# Free surface models + diff --git a/main/_sources/Bibliography.md b/main/_sources/Bibliography.md new file mode 100644 index 0000000..7ef9ec3 --- /dev/null +++ b/main/_sources/Bibliography.md @@ -0,0 +1,29 @@ +# Bibliography + + + +```{bibliography} + + + + +``` + +--- + +
+
+
+
+
+ +[.](CiteEveryReference.md) \ No newline at end of file diff --git a/main/_sources/CiteEveryReference.md b/main/_sources/CiteEveryReference.md new file mode 100644 index 0000000..228f396 --- /dev/null +++ b/main/_sources/CiteEveryReference.md @@ -0,0 +1,8 @@ + + +{cite}`moresiComputationalApproachesStudying2007` \ No newline at end of file diff --git a/main/_sources/Exercises/Figures/README.md b/main/_sources/Exercises/Figures/README.md new file mode 100644 index 0000000..21af31b --- /dev/null +++ b/main/_sources/Exercises/Figures/README.md @@ -0,0 +1,2 @@ +Figures associated with "Exercises" for the jupyterbook + diff --git a/main/_sources/Exercises/README.md b/main/_sources/Exercises/README.md new file mode 100644 index 0000000..edc87b8 --- /dev/null +++ b/main/_sources/Exercises/README.md @@ -0,0 +1,16 @@ +# Exercises + +Add questions to include in the JB ... don't add answers because a private repo +cannot be clones to jupyterhub with nbgitpuller. + +The use of general "admonitions" is quite a good format for individual questions and it is useful to separate them out into a separate markdown file if you want to create a printable pdf. + +::::{admonition} Q1 - How to create a question + +Suppose you want to create a series of textbook style questions for your jupyterbook. +The goal might be just to have a bunch of questions to choose from or it may be that you would +like to release a worksheet. + +Try using a general jupyterbook admonition to create a question card and see if that works for you. + +:::: \ No newline at end of file diff --git a/main/_sources/Figures/Diagrams/README.md b/main/_sources/Figures/Diagrams/README.md new file mode 100644 index 0000000..35f78d7 --- /dev/null +++ b/main/_sources/Figures/Diagrams/README.md @@ -0,0 +1,13 @@ +# Diagrams / Figures + + + +Note: for sketches / figures, please convert files to inkscape-compatible format at some point + +## Figure captions / descriptions. + + - **NS_Benchmark_DFG_2.png**: this is the benchmark for Reynolds number 100 in which the flow is a periodic shedding of vortices. + This figure shows the flow and flow markers two periods apart in a low resolution calculation. In the upper image of this figure, the vortex that is just being + shed from the top side of the cylinder is just about to be swept out of the domain in the lower image. UW is clearly capturing the + periodicity well but the benchmark also demands accurate computation of lift, drag, pressure, frequency etc. + diff --git a/main/_sources/Figures/PDFs/README.md b/main/_sources/Figures/PDFs/README.md new file mode 100644 index 0000000..ad99c63 --- /dev/null +++ b/main/_sources/Figures/PDFs/README.md @@ -0,0 +1 @@ +This folder is for PDF files that can be embedded in the book (as a presentation, for example). \ No newline at end of file diff --git a/main/_sources/FrontPage.md b/main/_sources/FrontPage.md new file mode 100644 index 0000000..279f489 --- /dev/null +++ b/main/_sources/FrontPage.md @@ -0,0 +1,60 @@ + +# The Underworld Geodynamics Platform + +```{warning} +Warning - Underworld 3 is still in $\beta$ release +``` + +![](./Figures/SocialShare.png) + +## A parallel, python, particle-in-cell, finite-element code for Geodynamics + +Underworld is a python-friendly geodynamics code which provides a programmable and flexible front end to all the functionality of the code running in a parallel HPC environment. This gives significant advantages to the user, with access to the power of python libraries for setup of complex problems, analysis at runtime, problem steering, and coupling of multiple problems. Underworld is integrated with the literate programming environment of the jupyter notebook system for tutorials and as a teaching tool for solid Earth geoscience. + +Underworld is an open-source, particle-in-cell finite element code tuned for large-scale geodynamics simulations. The numerical algorithms allow the tracking of history information through the high-strain deformation associated with fluid flow (for example, transport of the stress tensor in a viscoelastic, convecting medium, or the advection of fine-scale damage parameters by the large-scale flow). The finite element mesh can be static or dynamic, but it is not constrained to move in lock-step with the evolving geometry of the fluid. This hybrid approach is very well suited to complex fluids which is how the solid Earth behaves on a geological timescale. + +#### Links + + - [Underworld Blog](https://www.underworldcode.org/articles) + - [Underworld on Github](https://github.com/underworldcode/underworld3) + - [Underworld documentation](https://underworldcode.github.io/underworld3/main) + - [Underworld api documentation](https://underworldcode.github.io/underworld3/main_api) + - [Underworld beta documentation](https://underworldcode.github.io/underworld3) + + +### Governance + +Underworld is funded by AuScope which is part of the Australian Government's NCRIS initiative to provide community research infrastructure (please see www.auscope.org.au for more information). + +The Underworld development team is based in Australia at the Australian National University, the University of Sydney and at Monash University and is led by Louis Moresi (ANU). + +All development is overseen by a steering committee drawn from the supporting organisations and representatives from the Underworld community. + +### Background + +The numerical methods have been published in detail in Moresi et al, (2002, 2003). These papers dealt primarily with 2D applications but in recent years, we have introduced a number of improvements in the method to enable us to scale the problem to 3D (Moresi et al, 2007). For example we use a fast discrete Voronoi method to compute the integration weights of the particle-to-mesh mapping efficiently (Velic et al, 2009). We have also concentrated on extremely robust solvers / preconditioners which are necessary because the material variations and geometrical complexity are both large and unpredictable when we start of the simulation. + +The benefit of this approach is associated with the separation of the computational mesh from the swarm of points which track the history. This allows us to retain a much more structured computational mesh than the deformation / material history would otherwise allow. We can take full advantage of the most efficient geometrical multigrid solvers and there is no need to preserve structure during any remeshing operations we undertake (for example if we do need to track a free surface or an internal interface). Although there are several complexities introduced by enforcing this separation, we find that the benefits, for our particular class of problems, are significant. + +### Implementation and parallelism + +The numerical solvers are based around the PETSc software suite which focuses on delivering good parallel scalability (up to thousands-of-cores). Our experience to date shows good scalability 2000+ cores + + +## Acknowledgements + +We would like to acknowledge AuScope Simulation, Analysis and Modelling for providing long term funding which has made the project possible. Additional funding for specific improvements and additional functionality has come from the Australian Research Council (http://www.arc.gov.au). The python toolkit was funded by the NeCTAR eresearch_tools program. Underworld2 was originally developed in collaboration with the Victorian Partnership for Advanced Computing. + +*The documentation and tutorial materials provided by the authors are open source under a creative commons licence. +We acknowledge the contribution of the community in providing other materials and we endeavour to provide the correct attribution and citation. Please contact louis.moresi@anu.edu.au for updates and corrections.* + +--- + +## Accessibility + +   + + +The html can also be typeset using the [Atkinson Hyperlegible](https://brailleinstitute.org/freefont) font everywhere, other than monospaced computer code, as an aid to legibility. This button is also located at the bottom of the left navigation bar on every page and will toggle between settings. + + diff --git a/main/_sources/Glossary.md b/main/_sources/Glossary.md new file mode 100644 index 0000000..23ad878 --- /dev/null +++ b/main/_sources/Glossary.md @@ -0,0 +1,19 @@ +# Glossary + +Below are some important definitions. + +```{glossary} +Cloud Computing + the on-demand availability of computer system resources, especially data storage (cloud storage) and computing power, without direct active management by the user.[2] Large clouds often have functions distributed over multiple locations, each location being a data center. Cloud computing relies on sharing of resources to achieve coherence and economies of scale (Wikipedia) + +FEM + The Finite Element Method + +JupyterHub + brings the power of notebooks to groups of users. It gives users access to computational environments and resources without burdening the users with installation and maintenance tasks. Users - including students, researchers, and data scientists - can get their work done in their own workspaces on shared resources which can be managed efficiently by system administrators. + + +``` + + + diff --git a/main/_sources/Lectures/Index.md b/main/_sources/Lectures/Index.md new file mode 100644 index 0000000..e5541a6 --- /dev/null +++ b/main/_sources/Lectures/Index.md @@ -0,0 +1,17 @@ +# Lectures and Tutorials for Underworld 3 + +*This is named "Lectures" because it comes from the course-notes template but here it really stands for tutorial material.* + + + +Two examples of lecture notes and simple slideshows that we can embed. + +The lecture notes are in standard `myst markdown` format like the rest of the book. + +The slideshows are built along with the book using `reveal-md`. They are supposed to +be very simple, versionable (text based, not binaries) and rely on external links for +complicated media such as large movies. + +[Lecture 1](Lecture_example_1) is a demonstration + +[Lecture 2](Lecture_example_2) has a bit of information about the `reveal-md` slides \ No newline at end of file diff --git a/main/_sources/Lectures/Lecture_example_1.md b/main/_sources/Lectures/Lecture_example_1.md new file mode 100644 index 0000000..edbc3b6 --- /dev/null +++ b/main/_sources/Lectures/Lecture_example_1.md @@ -0,0 +1,14 @@ +# Lecture 1 - Notes + +A lecture can have some notes and a slideshow. + + + +The embedding is via an `html iframe` that points to the built path (all the slides are rendered into the +slideshows directory at the `root` level of the book) + +```html + +``` + + diff --git a/main/_sources/Lectures/Lecture_example_2.md b/main/_sources/Lectures/Lecture_example_2.md new file mode 100644 index 0000000..d726a16 --- /dev/null +++ b/main/_sources/Lectures/Lecture_example_2.md @@ -0,0 +1,103 @@ +# Lecture 2 - Slides + + +This is an embedded slide show that will show you a few tricks. + + + + +The embedding is via an `html iframe` that points to the built path (all the slides are rendered into the +slideshows directory at the `root` level of the book) + +```html + +``` + +## Source + +This is the source code for the slide deck. It is mostly (reveal-md) markdown with 1) a `yaml` header that has reveal options in it, 2) html tags that use `reveal.js` styling. Note that it is relatively easy to make a few slides this way but more than this can become complicated. + +`````markdown +separator: '<--o--> ' +verticalSeparator: '<--v-->' +revealOptions: +# transition: 'fade' + + slideNumber: true + width: 1100 + height: 750 + margin: 0.07 +--- + +# Slides + +- Louis Moresi +- Australian National University + +<--o--> + +## Slide 2 + +Typically, we have one or two images on a slide + + + +and text that explains what is going on. +The markdown image tags are limiting but `reveal.js` has image +classes that can be used directly without too much bother: + +```html + +``` + +<--o--> + +## Slide 3 + +Animations / styling work using `reveal.js` classes + +

Fade in

+

Fade out

+

Highlight red

+

Fade in, then out

+

Slide up while fading in

+ +<--o--> + +## Slide 4 Math + +Mathematics via *mathjax* + +$$ e^{i\pi} + 1 = 0$$ + +With inline available ($e^{i\pi} = -1$) as well + +<--o--> + +## Slide 5 Vertical slides + +Reveal has vertical sub-stacks that you can divert through + + - Vertical stack 1 + +<--v--> + +## Slide 5.1 Vertical slides + + +Reveal has vertical sub-stacks that you can divert through + + - Vertical stack 2 + +<--v--> + +## Slide 5.2 Vertical slides + +Reveal has vertical sub-stacks that you can divert through + +![Earth](images/LithosphereThickness.png) + +<--o--> + +````` + diff --git a/main/_sources/Lectures/LecturesFolder.md b/main/_sources/Lectures/LecturesFolder.md new file mode 100644 index 0000000..f2555a6 --- /dev/null +++ b/main/_sources/Lectures/LecturesFolder.md @@ -0,0 +1,4 @@ +# Lectures & Presentations + +`reveal-md` and embedded PDF are supported for lectures or tutorial presentations that are included in this book. + diff --git a/main/_sources/Lectures/movies/README.md b/main/_sources/Lectures/movies/README.md new file mode 100644 index 0000000..b1079df --- /dev/null +++ b/main/_sources/Lectures/movies/README.md @@ -0,0 +1 @@ +I wonder what goes in this folder ? \ No newline at end of file diff --git a/main/_sources/Lectures/static_slides/slideshows/movies/README.md b/main/_sources/Lectures/static_slides/slideshows/movies/README.md new file mode 100644 index 0000000..b1079df --- /dev/null +++ b/main/_sources/Lectures/static_slides/slideshows/movies/README.md @@ -0,0 +1 @@ +I wonder what goes in this folder ? \ No newline at end of file diff --git a/main/_sources/Manual/ConstitutiveModels.md b/main/_sources/Manual/ConstitutiveModels.md new file mode 100644 index 0000000..8a9b060 --- /dev/null +++ b/main/_sources/Manual/ConstitutiveModels.md @@ -0,0 +1,2 @@ +# Constitutive Models + diff --git a/main/_sources/Manual/Index.md b/main/_sources/Manual/Index.md new file mode 100644 index 0000000..8d7df0d --- /dev/null +++ b/main/_sources/Manual/Index.md @@ -0,0 +1,22 @@ +# Introduction to Underworld 3 + +```{margin} +![](./Figures/MansoursNightmare.png) + + +Come into the garden, Maud
+Come into the garden, Maud
+I am here at the gate alone;
+And the woodbine spices are wafted abroad,
+And the musk of the rose is blown.
+
+Alfred, Lord Tennyson +
+``` + +An introduction to the components of an underworld model: + - [Meshing & Coordinate Systems](Meshes) + - [Sympy interface & Functions](Sympy) + - [Mesh Variables](VariablesAndFunctions) + - Swarm Variables + - Constitutive Models diff --git a/main/_sources/Manual/Meshes.md b/main/_sources/Manual/Meshes.md new file mode 100644 index 0000000..d01735e --- /dev/null +++ b/main/_sources/Manual/Meshes.md @@ -0,0 +1,6 @@ +# Meshes and model objects + +The most fundamental object within Underworld is the `Mesh` object. This object describes the geometry, boundary conditions, coordinate system, holds discrete variables (structured on the mesh or unstructured swarms). + + + diff --git a/main/_sources/Manual/Solvers.md b/main/_sources/Manual/Solvers.md new file mode 100644 index 0000000..81a3e0f --- /dev/null +++ b/main/_sources/Manual/Solvers.md @@ -0,0 +1,3 @@ +# Solver Objects + +Templates for equations systems. \ No newline at end of file diff --git a/main/_sources/Manual/Swarms.md b/main/_sources/Manual/Swarms.md new file mode 100644 index 0000000..5f9dd66 --- /dev/null +++ b/main/_sources/Manual/Swarms.md @@ -0,0 +1,23 @@ +# Particle Swarms + +## Swarm Variables + +Swarm variables are piecewise continuous functions that cannot be integrated directly +with the current pointwise-function requirements of PETScDS. Instead we map the +piecewise function to a continuous, mesh variable which we refer to as a *proxy* swarm +variable. + +$$ \int_\Omega \phi u_n = \int_\Omega \phi u_p $$ + +The left hand side of this equation can be assembled using the standard finite element +machinery of PETScDS, but, the right hand side must be integrated in a manner that accounts +for the piecewise nature of the swarm variable information. + +One way in which we can build the right hand side is to use an inverse-distance weighted interpolation to an intermediary mesh variable (of comparable spatial density to the swarm, or higher polynomial degree). Alternatively, we can form a piecewise-pointwise function that matches the PETScDS template, although, strictly, this falls outside the restrictions imposed by that framework. + +At first it can seem over-complicated to introduce another equation system that needs to be solved in order to use Lagrangian particle information interchangeably with mesh variables but there are some distinct advantages. In the traditional particle-in-cell or material-point finite element method, there is an ambiguity in the way in which we map Lagrangian variables to their mesh equivalents. We rely on an average over the path of a particle to cancel fluctuations that occur due to uneven particle distributions relative to integration points or nodal points. There is no consistent way for us to ensure that constraints are correctly applied, boundary conditions are satisfied or null-space modes are damped. However, these constraints can all be included in the projection equation to ensure that the proxy variables are compatible with the other mesh-variables used to build systems of equations. + +In the code, we substitute proxy variables at the just-in-time compilation stage automatically. + +*NOTE: - currently we don't really do that, but we probably should: swarm variable, boundary conditions, bespoke integration ... do all that in each update phase.* + diff --git a/main/_sources/Manual/Sympy.md b/main/_sources/Manual/Sympy.md new file mode 100644 index 0000000..524d28d --- /dev/null +++ b/main/_sources/Manual/Sympy.md @@ -0,0 +1,3 @@ +# Sympy Interface in Underworld + +The interface between the underworld3 parallel data model and the user is through a combination of symbolic manupulation of data objects through mathematical expressions using `sympy`and access to raw data through a `numpy` \ No newline at end of file diff --git a/main/_sources/Manual/VariablesAndFunctions.md b/main/_sources/Manual/VariablesAndFunctions.md new file mode 100644 index 0000000..33be3f9 --- /dev/null +++ b/main/_sources/Manual/VariablesAndFunctions.md @@ -0,0 +1,6 @@ +# Underworld Variables + +## Mesh Variables + + + diff --git a/main/_sources/Notebooks/.virtual_documents/Examples-StokesFlow/Ex_Stokes_Cartesian_SolC.py b/main/_sources/Notebooks/.virtual_documents/Examples-StokesFlow/Ex_Stokes_Cartesian_SolC.py new file mode 100644 index 0000000..d00dbc0 --- /dev/null +++ b/main/_sources/Notebooks/.virtual_documents/Examples-StokesFlow/Ex_Stokes_Cartesian_SolC.py @@ -0,0 +1,347 @@ +# %% +import petsc4py +from petsc4py import PETSc + +# options = PETSc.Options() +# options["help"] = None + +import os + +os.environ["UW_TIMING_ENABLE"] = "1" + + +import underworld3 as uw +from underworld3.systems import Stokes +from underworld3 import function +from underworld3 import timing + +import numpy as np + + +# %% +n_els = 4 +refinement = 3 + +mesh1 = uw.meshing.UnstructuredSimplexBox( + regular=True, + minCoords=(0.0, 0.0), + maxCoords=(1.0, 1.0), + cellSize=1 / n_els, + qdegree=3, + refinement=refinement, +) + +mesh2 = uw.meshing.StructuredQuadBox( + elementRes=(n_els, n_els), + minCoords=(0.0, 0.0), + maxCoords=(1.0, 1.0), + qdegree=3, + refinement=refinement, +) + +mesh = mesh1 + + +mesh.dm.view() + + +# %% +v = uw.discretisation.MeshVariable( + "v", mesh, mesh.dim, degree=2, varsymbol=r"\mathbf{u}" +) +p = uw.discretisation.MeshVariable( + "p", mesh, 1, degree=1, continuous=False, varsymbol=r"{p}" +) +T = uw.discretisation.MeshVariable( + "T", mesh, 1, degree=3, continuous=True, varsymbol=r"{T}" +) +T2 = uw.discretisation.MeshVariable( + "T2", mesh, 1, degree=3, continuous=True, varsymbol=r"{T_2}" +) + + +# %% +stokes = uw.systems.Stokes(mesh, velocityField=v, pressureField=p) +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel(v) +stokes.constitutive_model.Parameters.viscosity = 1 + + +# %% +# Set some things +import sympy +from sympy import Piecewise + + +x, y = mesh.X + + +mesh.get_min_radius() + + +hw = 0.1 * mesh.get_min_radius() +surface_fn = 2 * uw.maths.delta_function(y - 1, hw) / uw.maths.delta_function(0.0, hw) +base_fn = 2 * uw.maths.delta_function(y, hw) +right_fn = 2 * uw.maths.delta_function(x - 1, hw) +left_fn = 2 * uw.maths.delta_function(x, hw) + + +surface_fn + + +uw.function.evalf(surface_fn, np.array([[0.0, 1.0]])) + + +# options = PETSc.Options() +# options.getAll() + + +eta_0 = 1.0 +x_c = 0.5 +f_0 = 1.0 + + +stokes.penalty = 100.0 +stokes.bodyforce = sympy.Matrix( + [ + 0, + Piecewise( + (f_0, x > x_c), + (0.0, True), + ), + ] +) + + +# This is the other way to impose no vertical + +stokes.bodyforce[0] -= 1.0e6 * v.sym[0] * (left_fn + right_fn) +stokes.bodyforce[1] -= 1.0e3 * v.sym[1] * (surface_fn + base_fn) + +# stokes.add_natural_bc( -1.0e10 * v.sym[1], sympy.Matrix((0.0, 0.0)).T , "Top", components=[1]) + + +# free slip. +# note with petsc we always need to provide a vector of correct cardinality. + +stokes.add_dirichlet_bc((sympy.oo, 0.0), "Bottom") +stokes.add_dirichlet_bc((sympy.oo, 0.0), "Top") +stokes.add_dirichlet_bc((0.0, sympy.oo), "Left") +stokes.add_dirichlet_bc((0.0, sympy.oo), "Right") + + +# stokes.petsc_options["snes_rtol"] = 1.0e-6 +# stokes.petsc_options["ksp_rtol"] = 1.0e-6 +# stokes.petsc_options["snes_max_it"] = 10 + + +stokes.tolerance = 1.0e-3 + + +stokes.petsc_options["snes_monitor"] = None +stokes.petsc_options["ksp_monitor"] = None + + +stokes.petsc_options["snes_type"] = "newtonls" +stokes.petsc_options["ksp_type"] = "fgmres" + +# stokes.petsc_options.setValue("fieldsplit_velocity_pc_type", "mg") +stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_type", "kaskade") +stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_cycle_type", "w") + +stokes.petsc_options["fieldsplit_velocity_mg_coarse_pc_type"] = "svd" +stokes.petsc_options[f"fieldsplit_velocity_ksp_type"] = "fcg" +stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_type"] = "chebyshev" +stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_max_it"] = 7 +stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_converged_maxits"] = None + +# gasm is super-fast ... but mg seems to be bulletproof +# gamg is toughest wrt viscosity + +stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "gamg") +stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "additive") +stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v") + +# # # mg, multiplicative - very robust ... similar to gamg, additive + +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "mg") +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "multiplicative") +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v") + + +stokes._setup_pointwise_functions(verbose=True) +stokes._setup_discretisation(verbose=True) +stokes.dm.ds.view() + + +# %% +# Solve time +stokes.solve() + + +stokes._uu_G0 + + +# check the mesh if in a notebook / serial + +import mpi4py + +if mpi4py.MPI.COMM_WORLD.size == 1: + import numpy as np + import pyvista as pv + import vtk + + try: + pv.start_xvfb() + except OSError: + pass + + pv.global_theme.background = "white" + pv.global_theme.window_size = [250, 500] + pv.global_theme.anti_aliasing = "msaa" + pv.global_theme.jupyter_backend = "panel" + pv.global_theme.smooth_shading = True + + mesh.vtk("tmp_mesh.vtk") + pvmesh = pv.read("tmp_mesh.vtk") + + pvmesh.point_data["P"] = uw.function.evalf(p.sym[0], mesh.data) + pvmesh.point_data["V"] = uw.function.evalf(v.sym.dot(v.sym), mesh.data) + pvmesh.point_data["delta"] = uw.function.evalf(surface_fn, mesh.data) + + arrow_loc = np.zeros((stokes.u.coords.shape[0], 3)) + arrow_loc[:, 0:2] = stokes.u.coords[...] + + arrow_length = np.zeros((stokes.u.coords.shape[0], 3)) + arrow_length[:, 0] = uw.function.evalf(stokes.u.sym[0], stokes.u.coords) + arrow_length[:, 1] = uw.function.evalf(stokes.u.sym[1], stokes.u.coords) + + pl = pv.Plotter(window_size=[1000, 1000]) + pl.add_axes() + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="V", + use_transparency=False, + opacity=1.0, + ) + + pl.add_arrows(arrow_loc, arrow_length, mag=1) + + pl.show(cpos="xy") + + +stokes.bodyforce = sympy.Matrix( + [ + 0, + -sympy.cos(sympy.pi * x) + * sympy.sin(2 * sympy.pi * y) + * (1 - (surface_fn + base_fn)), + ] +) + +stokes.bodyforce[0] -= 1.0e3 * v.sym[0] * (left_fn + right_fn) +stokes.bodyforce[1] -= 1.0e3 * v.sym[1] * (surface_fn + base_fn) + +viscosity_fn = sympy.Piecewise( + (1.0e6, x > x_c), + (1.0, True), +) + +stokes.constitutive_model.Parameters.shear_viscosity_0 = viscosity_fn + + +stokes.saddle_preconditioner = sympy.simplify( + 1 / (stokes.constitutive_model.viscosity + stokes.penalty) +) + + +stokes._setup_pointwise_functions() +stokes._setup_discretisation() +stokes._u_f1 + + +timing.reset() +timing.start() + +stokes.solve(zero_init_guess=True) + +timing.print_table(display_fraction=0.999) + + +# check the mesh if in a notebook / serial + +import mpi4py + +if mpi4py.MPI.COMM_WORLD.size == 1: + import numpy as np + import pyvista as pv + import vtk + + pv.global_theme.background = "white" + pv.global_theme.window_size = [750, 1200] + pv.global_theme.anti_aliasing = "msaa" + pv.global_theme.jupyter_backend = "panel" + pv.global_theme.smooth_shading = True + + mesh.vtk("tmp_mesh.vtk") + pvmesh = pv.read("tmp_mesh.vtk") + + pvmesh.point_data["P"] = uw.function.evalf(p.sym[0], mesh.data) + pvmesh.point_data["V"] = uw.function.evalf(v.sym.dot(v.sym), mesh.data) + + arrow_loc = np.zeros((stokes.u.coords.shape[0], 3)) + arrow_loc[:, 0:2] = stokes.u.coords[...] + + arrow_length = np.zeros((stokes.u.coords.shape[0], 3)) + arrow_length[:, 0] = uw.function.evalf(stokes.u.sym[0], stokes.u.coords) + arrow_length[:, 1] = uw.function.evalf(stokes.u.sym[1], stokes.u.coords) + + pl = pv.Plotter(window_size=[1000, 1000]) + pl.add_axes() + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="V", + use_transparency=False, + opacity=1.0, + ) + + # pl.add_mesh(pvmesh, cmap="coolwarm", edge_color="Black", show_edges=True, scalars="T", + # use_transparency=False, opacity=1.0) + + pl.add_arrows(arrow_loc, arrow_length, mag=50) + + pl.show(cpos="xy") + + +# %% +try: + import underworld as uw2 + + solC = uw2.function.analytic.SolC() + vel_soln_analytic = solC.fn_velocity.evaluate(mesh.data) + from mpi4py import MPI + + comm = MPI.COMM_WORLD + from numpy import linalg as LA + + with mesh.access(v): + num = function.evaluate(v.fn, mesh.data) # this appears busted + if comm.rank == 0: + print("Diff norm a. = {}".format(LA.norm(v.data - vel_soln_analytic))) + print("Diff norm b. = {}".format(LA.norm(num - vel_soln_analytic))) + # if not np.allclose(v.data, vel_soln_analytic, rtol=1): + # raise RuntimeError("Solve did not produce expected result.") + comm.barrier() +except ImportError: + import warnings + + warnings.warn("Unable to test SolC results as UW2 not available.") + +# %% diff --git a/main/_sources/Notebooks/.virtual_documents/Examples-StokesFlow/Readme.md b/main/_sources/Notebooks/.virtual_documents/Examples-StokesFlow/Readme.md new file mode 100644 index 0000000..fd40910 --- /dev/null +++ b/main/_sources/Notebooks/.virtual_documents/Examples-StokesFlow/Readme.md @@ -0,0 +1,4 @@ + + + + diff --git a/main/_sources/Notebooks/Examples-Convection/Ex_AdvectionDiffusionSLCN_RotationTest.py b/main/_sources/Notebooks/Examples-Convection/Ex_AdvectionDiffusionSLCN_RotationTest.py new file mode 100644 index 0000000..706d499 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Convection/Ex_AdvectionDiffusionSLCN_RotationTest.py @@ -0,0 +1,307 @@ +# # Field (SemiLagrange) Advection solver test +# +# Shear flow driven by a pre-defined, rigid body rotation in a disc or by the boundary conditions +# + +# + +import petsc4py +from petsc4py import PETSc + +import os +import nest_asyncio +nest_asyncio.apply() + +os.environ["UW_TIMING_ENABLE"] = "1" + +import underworld3 as uw +from underworld3.systems import Stokes +from underworld3 import function +from underworld3 import VarType +from underworld3 import timing + +import numpy as np +import sympy + +options = PETSc.Options() + +# options.getAll() +# - +meshball = uw.meshing.Annulus( + radiusOuter=1.0, radiusInner=0.5, cellSize=0.2, refinement=1, qdegree=3 +) + + +# + +v_soln = uw.discretisation.MeshVariable("U", meshball, meshball.dim, degree=2) +t_soln = uw.discretisation.MeshVariable("T", meshball, 1, degree=3) +t_0 = uw.discretisation.MeshVariable("T0", meshball, 1, degree=3, varsymbol=r"T_{0}") + +# Create a temperature structure / buoyancy force + +import sympy + +radius_fn = sympy.sqrt( + meshball.rvec.dot(meshball.rvec) +) # normalise by outer radius if not 1.0 +unit_rvec = meshball.rvec / (1.0e-10 + radius_fn) + +# Some useful coordinate stuff + +x, y = meshball.X +r, th = meshball.CoordinateSystem.xR + +# Rigid body rotation v_theta = constant, v_r = 0.0 + +theta_dot = 2.0 * np.pi # i.e one revolution in time 1.0 +v_x = -r * theta_dot * sympy.sin(th) +v_y = r * theta_dot * sympy.cos(th) + +with meshball.access(v_soln): + v_soln.data[:, 0] = uw.function.evaluate(v_x, v_soln.coords) + v_soln.data[:, 1] = uw.function.evaluate(v_y, v_soln.coords) + +# + +# swarm = uw.swarm.Swarm(mesh=meshball) +# T1 = uw.swarm.SwarmVariable("Tminus1", swarm, 1) +# X1 = uw.swarm.SwarmVariable("Xminus1", swarm, 2) +# swarm.populate(fill_param=3) + + +# + +# Create adv_diff object + +# Set some things +k = 0.01 +h = 0.1 +t_i = 2.0 +t_o = 1.0 +r_i = 0.5 +r_o = 1.0 +delta_t = 1.0 +# - + + +adv_diff = uw.systems.AdvDiffusion( + meshball, + u_Field=t_soln, + V_fn=v_soln, + solver_name="adv_diff", + order=2, +) + + +adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel(adv_diff.Unknowns) +adv_diff.constitutive_model.Parameters.diffusivity = k + + +# + +# Define T boundary conditions via a sympy function + +import sympy + +abs_r = sympy.sqrt(meshball.rvec.dot(meshball.rvec)) +init_t = sympy.exp(-30.0 * (meshball.N.x**2 + (meshball.N.y - 0.75) ** 2)) + +adv_diff.add_dirichlet_bc(0.0, "Lower") +adv_diff.add_dirichlet_bc(0.0, "Upper") + +with meshball.access(t_0, t_soln): + t_0.data[...] = uw.function.evaluate(init_t, t_0.coords).reshape(-1, 1) + t_soln.data[...] = t_0.data[...] + + +# + +# # We can over-ride the swarm-particle update routine since we can integrate +# # the velocity field by hand. + +# with adv_diff._nswarm.access(): +# coords0 = adv_diff._nswarm.data.copy() + +# delta_t = 0.000 + +# n_x = uw.function.evaluate(r * sympy.cos(th - delta_t * theta_dot), coords0) +# n_y = uw.function.evaluate(r * sympy.sin(th - delta_t * theta_dot), coords0) + +# coords = np.empty_like(coords0) +# coords[:, 0] = n_x +# coords[:, 1] = n_y + +# # delta_t will be baked in when this is defined ... so re-define it +# adv_diff.solve(timestep=delta_t) # , coords=coords) +# - +def plot_T_mesh(filename): + if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + points = vis.meshVariable_to_pv_cloud(t_soln) + points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym) + + point_cloud = pv.PolyData(points) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym) + + pl = pv.Plotter(window_size=(1000, 750)) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.0001, opacity=0.75) + + pl.add_points( + point_cloud, + cmap="coolwarm", + render_points_as_spheres=False, + point_size=10, + opacity=0.66, + ) + + pl.add_mesh(pvmesh, "Black", "wireframe", opacity=0.75) + + pl.remove_scalar_bar("T") + pl.remove_scalar_bar("mag") + + pl.screenshot( + filename="{}.png".format(filename), + window_size=(1280, 1280), + return_img=False, + ) + + pl.show() + + +t_soln2 = uw.discretisation.MeshVariable( + "U2", meshball, vtype=uw.VarType.SCALAR, degree=2 +) + +adv_diff.estimate_dt() + + +adv_diff.DuDt._psi_star_projection_solver._constitutive_model + +# + +timing.reset() +timing.start() + +delta_t = 0.001 + +adv_diff.solve(timestep=delta_t, verbose=False, _force_setup=False) + +# + +# check the mesh if in a notebook / serial + + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + points = vis.meshVariable_to_pv_cloud(t_soln) + points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym) + + point_cloud = pv.PolyData(points) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym) + + pl = pv.Plotter(window_size=(1000, 750)) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.01, opacity=0.75) + pl.add_points( + point_cloud, + cmap="coolwarm", + render_points_as_spheres=True, + point_size=7, + opacity=0.66, + ) + pl.add_mesh(pvmesh, "Black", "wireframe", opacity=0.75) + + # pl.remove_scalar_bar("T") + pl.remove_scalar_bar("mag") + + pl.show() + +# + +# Advection/diffusion model / update in time + +expt_name = "rotation_test_slcn" + +delta_t = 0.05 + +plot_T_mesh(filename="{}_step_{}".format(expt_name, 0)) + +for step in range(0, 10): + # delta_t will be baked in when this is defined ... so re-define it + adv_diff.solve(timestep=delta_t, verbose=False) + + # stats then loop + + # tstats = t_soln.stats() + + if uw.mpi.rank == 0: + print("Timestep {}, dt {}".format(step, delta_t)) + # print(tstats) + + plot_T_mesh(filename="{}_step_{}".format(expt_name, step)) + + # savefile = "output_conv/convection_cylinder_{}_iter.h5".format(step) + # meshball.save(savefile) + # v_soln.save(savefile) + # t_soln.save(savefile) + # meshball.generate_xdmf(savefile) +# - + + +t_soln.stats() + +# + +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + + pvmesh = vis.mesh_to_pv_mesh(meshball) + points = vis.meshVariable_to_pv_cloud(t_soln) + points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym) + points.point_data["dT"] = vis.scalar_fn_to_pv_points(points, t_soln.sym) - vis.scalar_fn_to_pv_points(points, t_0.sym) + + point_cloud = pv.PolyData(points) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym) + + pl = pv.Plotter(window_size=(1000, 750)) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.0001, opacity=0.75) + + pl.add_points( + point_cloud, + cmap="coolwarm", + scalars="T", # clim=[-0.2,0.2], + render_points_as_spheres=False, + point_size=10, + opacity=0.66, + ) + + pl.add_mesh(pvmesh, "Black", "wireframe", opacity=0.75) + + # pl.remove_scalar_bar("T") + pl.remove_scalar_bar("mag") + + pl.show() + +# + +# savefile = "output_conv/convection_cylinder.h5".format(step) +# meshball.save(savefile) +# v_soln.save(savefile) +# t_soln.save(savefile) +# meshball.generate_xdmf(savefile) +# - +uw.timing.print_table() + + +# diff --git a/main/_sources/Notebooks/Examples-Convection/Ex_AdvectionDiffusionSwarm_RotationTest.py b/main/_sources/Notebooks/Examples-Convection/Ex_AdvectionDiffusionSwarm_RotationTest.py new file mode 100644 index 0000000..b4918f4 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Convection/Ex_AdvectionDiffusionSwarm_RotationTest.py @@ -0,0 +1,275 @@ +# # Swarm Advection solver test - shear flow driven by a pre-defined, rigid body rotation in a disc +# +# This example uses the Swarm advection approach rather than SLCN + +# + +import petsc4py +from petsc4py import PETSc + +import nest_asyncio +nest_asyncio.apply() + +import underworld3 as uw +from underworld3.systems import Stokes +from underworld3 import function + +import numpy as np + +options = PETSc.Options() +# options["help"] = None +# options["pc_type"] = "svd" +# options["dm_plex_check_all"] = None + +# import os +# os.environ["SYMPY_USE_CACHE"]="no" + +# options.getAll() +# + +import meshio + +meshball = uw.meshing.Annulus( + radiusOuter=1.0, radiusInner=0.5, cellSize=0.2, refinement=1, qdegree=3 +) +x, y = meshball.X +# - + + +v_soln = uw.discretisation.MeshVariable("U", meshball, meshball.dim, degree=2) +t_soln = uw.discretisation.MeshVariable("T", meshball, 1, degree=3) +t_soln_dt = uw.discretisation.MeshVariable("Tdt", meshball, 1, degree=3) +t_0 = uw.discretisation.MeshVariable("T0", meshball, 1, degree=3) + + +DTdt = uw.systems.Lagrangian_DDt( + meshball, + psi_fn = t_soln.sym, + V_fn = v_soln.sym, + vtype = uw.VarType.SCALAR, + degree = 1, + order = 1, + continuous=True, + varsymbol=r'T_s', + fill_param=3, +) + + +# check that the swarm variable works as a continuous field as well +DTdt.psi_star[0].sym.jacobian(meshball.X) + +# + +# Create adv_diff object + +# Set some things +k = 0.01 +h = 0.1 +t_i = 2.0 +t_o = 1.0 +r_i = 0.5 +r_o = 1.0 +delta_t = 1.0 + + +# + +adv_diff = uw.systems.AdvDiffusion( + meshball, + u_Field=t_soln, + V_fn = v_soln, + DuDt = DTdt, + solver_name="adv_diff_swarms", # not needed if coords is provided + order=1, +) + +adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel +adv_diff.constitutive_model.Parameters.diffusivity = k + + +# + +# Create a density structure / bu()oyancy force +# gravity will vary linearly from zero at the centre +# of the sphere to (say) 1 at the surface + +import sympy + +radius_fn = sympy.sqrt( + meshball.rvec.dot(meshball.rvec) +) # normalise by outer radius if not 1.0 +unit_rvec = meshball.rvec / (1.0e-10 + radius_fn) + +# Some useful coordinate stuff + +x, y = meshball.X +r, th = meshball.CoordinateSystem.xR + +# Rigid body rotation v_theta = constant, v_r = 0.0 + +theta_dot = 2.0 * np.pi # i.e one revolution in time 1.0 +v_x = -r * theta_dot * sympy.sin(th) +v_y = r * theta_dot * sympy.cos(th) + +with meshball.access(v_soln): + v_soln.data[:, 0] = uw.function.evaluate(v_x, v_soln.coords) + v_soln.data[:, 1] = uw.function.evaluate(v_y, v_soln.coords) + +# + +# Define T boundary conditions via a sympy function + +import sympy + +abs_r = sympy.sqrt(meshball.rvec.dot(meshball.rvec)) + +init_t = sympy.exp(-30.0 * (meshball.N.x**2 + (meshball.N.y - 0.75) ** 2)) + +adv_diff.add_dirichlet_bc(0.0, "Lower") +adv_diff.add_dirichlet_bc(0.0, "Upper") + +with meshball.access(t_0, t_soln): + t_0.data[...] = uw.function.evaluate(init_t, t_0.coords).reshape(-1, 1) + t_soln.data[...] = t_0.data[...] + + + +# + +# Validation - small timestep + +# delta_t = 0.01 +# adv_diff.solve(timestep=delta_t) +# - + + +def plot_T_mesh(filename): + + if uw.mpi.size == 1: + + import numpy as np + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + swarm_points = vis.swarm_to_pv_cloud(adv_diff.DuDt.swarm) + tsoln_points = vis.meshVariable_to_pv_cloud(t_soln) + + swarm_points.point_data["T"] = vis.scalar_fn_to_pv_points(swarm_points,adv_diff.DuDt.psi_fn) + + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh,t_soln.sym) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh,v_soln.sym) + + pl = pv.Plotter(window_size=(1000, 750)) + + pl.add_arrows(pvmesh.points, pvmesh.point_data["V"], mag=0.0001, opacity=0.75) + + # pl.add_points( + # swarm_points, + # cmap="coolwarm", + # render_points_as_spheres=False, + # point_size=20, + # opacity=0.66, + # ) + + pl.add_mesh(pvmesh, cmap="coolwarm", opacity=0.75) + + pl.screenshot( + filename="{}.png".format(filename), + window_size=(1280, 1280), + return_img=False, + ) + + # pl.show() + + +# + +with meshball.access(t_0, t_soln): + t_0.data[...] = uw.function.evaluate(init_t, t_0.coords).reshape(-1, 1) + t_soln.data[...] = t_0.data[...] + + + + +# + +adv_diff.DuDt.update(dt=0.05) + +# # Update the swarm locations +# swarm.advection( +# v_soln.sym, +# delta_t=0.05, +# corrector=False, +# restore_points_to_domain_func=meshball.return_coords_to_bounds, +# ) + +# + +# Advection/diffusion model / update in time + +delta_t = 0.05 +expt_name = "output/rotation_test_k_001" + +plot_T_mesh(filename="{}_step_{}".format(expt_name, 0)) + +for step in range(0, 10): + + adv_diff.solve(timestep=delta_t, verbose=False) + + tstats = t_soln.stats() + print("psi*", adv_diff.DuDt.psi_star[0]._meshVar.stats()) + + if uw.mpi.rank == 0: + print("Timestep {}, dt {}".format(step, delta_t)) + print(tstats) + + plot_T_mesh(filename="{}_step_{}".format(expt_name, step)) + + # savefile = "output_conv/convection_cylinder_{}_iter.h5".format(step) + # meshball.save(savefile) + # v_soln.save(savefile) + # t_soln.save(savefile) + # meshball.generate_xdmf(savefile) + + +# + +# check the mesh if in a notebook / serial + + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + swarm_points = vis.swarm_to_pv_cloud(adv_diff.DuDt.swarm) + tsoln_points = vis.meshVariable_to_pv_cloud(t_soln) + + swarm_points.point_data["Ts"] = vis.scalar_fn_to_pv_points(swarm_points, adv_diff.DuDt.psi_star[0].sym[0] ) + + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh,t_soln.sym) + pvmesh.point_data["Ts"] = vis.scalar_fn_to_pv_points(pvmesh,adv_diff.DuDt.psi_star[0].sym[0]) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh,v_soln.sym) + + pl = pv.Plotter(window_size=(1000, 750)) + + pl.add_arrows(pvmesh.points, pvmesh.point_data["V"], mag=0.02, opacity=0.75) + + pl.add_points( + swarm_points, + cmap="coolwarm", + render_points_as_spheres=False, + scalars="Ts", + point_size=10, + opacity=0.66, + ) + + # pl.add_mesh(pvmesh, cmap="coolwarm", opacity=0.75, scalars="T") + + # pl.remove_scalar_bar("T") + # pl.remove_scalar_bar("mag") + + pl.show() + +# + +# savefile = "output_conv/convection_cylinder.h5".format(step) +# meshball.save(savefile) +# v_soln.save(savefile) +# t_soln.save(savefile) +# meshball.generate_xdmf(savefile) +# - + +DTdt.psi_fn + + diff --git a/main/_sources/Notebooks/Examples-Convection/Ex_AdvectionDiffusion_1dBlock.py b/main/_sources/Notebooks/Examples-Convection/Ex_AdvectionDiffusion_1dBlock.py new file mode 100644 index 0000000..22d31c3 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Convection/Ex_AdvectionDiffusion_1dBlock.py @@ -0,0 +1,278 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Advection-diffusion of a hot pipe +# +# - Using the adv_diff solver. +# - Advection of the rectangular pulse vertically as it also diffuses. The velocity is 0.05 and has a diffusivity value of 1, 0.1 or 0.01 +# - Benchmark comparison between 1D analytical solution and 2D UW numerical model. +# +# ![](Figures/AdvectionTestFigure.png) +# +# *Figure: typical results from this test. Quad mesh v. unstructured triangles with equivalent +# resolution. $\kappa=1$, $\mathbf{v}=(1000,0)$, $t_0 = 0.0001$, $\delta t = 0.0003$. The error looks significantly larger with triangles but you can see that it is dominated by a relatively small* phase error *where the speed of propagation is slightly different from the analytic case.* +# +# +# ## How to test advection or diffusion only +# - Set velocity to 0 to test diffusion only. +# - Set diffusivity (k) to 0 to test advection only. +# +# +# ## Analytic solution +# +# $$ +# T(x,t) = +# \frac{\operatorname{erf}{\left(\frac{- \mathrm{x} + v \left(t + {t_0}\right) + \frac{{\delta}}{2} + {x_0}}{2 \sqrt{\kappa \left(t + {t_0}\right)}} \right)}}{2} + \frac{\operatorname{erf}{\left(\frac{\mathrm{x} - v \left(t + {t_0}\right) + \frac{{\delta}}{2} - {x_0}}{2 \sqrt{\kappa \left(t + {t_0}\right)}} \right)}}{2} +# $$ +# +# Where $x,y$ describe the coordinate frame, $v$ is the horizontal velocity that advects the temperature, $\delta$ is the width of the temperature anomaly, $x_0$ is the initial midpoint of the temperature anomaly. $\kappa$ is the thermal diffusivity, $t_0$ is the time at which we turn on the horizontal velocity. +# +# Note: this solution is derived from the diffusion of a step which is applied to the leading and trailing edges of the block. The solution is valid while the diffusion fronts from each interface remain independent of each other. (This is ill-defined from the problem, but the most obvious test is to look a the time that the block temperature drops below 1 to the tolerance of the solver). +# + +import nest_asyncio +nest_asyncio.apply() + +# + +import underworld3 as uw +import numpy as np +import sympy +import math +import os + +from scipy import special +# - + +if uw.mpi.size == 1: + import matplotlib.pyplot as plt + + +# ### Set up variables of the model + +# + +import sys + +init_t = 0.0001 +dt = 0.0003 +velocity = 1000. +centre = 0.2 +width = 0.2 + + +### min and max temps +tmin = 0. # temp min +tmax = 1.0 # temp max + +# I think we should get into the habit of doing this consistently with the PETSc interface + +res = uw.options.getReal("model_resolution", default=16) +kappa = uw.options.getInt("kappa", default=1.0) +Tdegree = uw.options.getInt("Tdeg", default=3) +Vdegree = uw.options.getInt("Vdeg", default=2) +simplex = uw.options.getBool("simplex", default=True) + + + +# Tdegree = int(sys.argv[1]) +# Vdegree = int(sys.argv[2]) +# kappa = float(sys.argv[3]) # 1, 0.1, 0.01 # diffusive constant +# res = int(sys.argv[4]) +# simplex = sys.argv[5].lower() + +# + +outputPath = f'./output/adv_diff-hot_pipe/' + +if uw.mpi.rank == 0: + # checking if the directory + if not os.path.exists(outputPath): + os.makedirs(outputPath) +# - + +# ### Set up the mesh + +xmin, xmax = 0, 1 +ymin, ymax = 0, 0.2 + +## Quads +if simplex == True: + mesh = uw.meshing.UnstructuredSimplexBox( + minCoords=(xmin, ymin), maxCoords=(xmax, ymax), cellSize=(ymax-ymin)/res, regular=False, qdegree=max(Tdegree, Vdegree) ) +else: + mesh = uw.meshing.StructuredQuadBox( + elementRes=(int(res)*5, int(res)), minCoords=(xmin, ymin), maxCoords=(xmax, ymax), qdegree=max(Tdegree, Vdegree), + ) + + +# ### Create mesh variables +# To be used in the solver + +# + +x,y = mesh.X + +x0 = sympy.symbols(r"{x_0}") +t0 = sympy.symbols(r"{t_0}") +delta = sympy.symbols(r"{\delta}") +ks = sympy.symbols(r"\kappa") +ts = sympy.symbols("t") +vs = sympy.symbols("v") + +Ts = ( sympy.erf( (x0 + delta/2 - x+(vs*(ts+t0))) / (2*sympy.sqrt(ks*(ts+t0)))) + sympy.erf( (-x0 + delta/2 + x-((ts+t0)*vs)) / (2*sympy.sqrt(ks*(ts+t0)))) ) / 2 +Ts + + +# + +def build_analytic_fn_at_t(time): + fn = Ts.subs({vs:velocity, ts:time, ks:kappa, delta:width, x0:centre, t0:init_t}) + return fn + + +Ts0 = build_analytic_fn_at_t(time=0.0) +TsVKT = build_analytic_fn_at_t(time=dt) + +# + +# Create the mesh var +T = uw.discretisation.MeshVariable("T", mesh, 1, degree=Tdegree) + +# This is the velocity field + +v = sympy.Matrix([velocity, 0]) +# - + +# #### Create the advDiff solver + +adv_diff = uw.systems.AdvDiffusionSLCN( + mesh, + u_Field=T, + V_fn=v, + solver_name="adv_diff", +) + +# ### Set up properties of the adv_diff solver +# - Constitutive model (Diffusivity) +# - Boundary conditions +# - Internal velocity +# - Initial temperature distribution + +adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel +adv_diff.constitutive_model.Parameters.diffusivity = kappa + +adv_diff.add_dirichlet_bc(tmin, "Left") +adv_diff.add_dirichlet_bc(tmin, "Right") + + +adv_diff.estimate_dt(v_factor=10) +steps = int(dt // (10*adv_diff.estimate_dt())) + +# ### Create points to sample the UW results + +# + +### get the initial temp profile + +with mesh.access(T): + T.data[:,0] = uw.function.evalf(Ts0, T.coords) + +# - + +step = 0 +model_time = 0.0 + +# + +adv_diff.petsc_options["snes_monitor_short"] = None + +# if uw.mpi.size == 1: +# adv_diff.petsc_options['pc_type'] = 'lu' + +# - + +while model_time < dt: + adv_diff.solve(timestep=dt/steps, zero_init_guess=False) + model_time += dt/steps + step += 1 + print(f"Timestep: {step}, model time {model_time}") + + +# %% +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, sympy.Matrix([velocity, 0]).T) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, T.sym) + pvmesh.point_data["Ta"] = vis.scalar_fn_to_pv_points(pvmesh, Ts0) + pvmesh.point_data["dT"] = pvmesh.point_data["T"] - pvmesh.point_data["Ta"] + + T_points = vis.meshVariable_to_pv_cloud(T) + T_points.point_data["T"] = vis.scalar_fn_to_pv_points(T_points, T.sym) + T_points.point_data["Ta"] = vis.scalar_fn_to_pv_points(T_points, TsVKT) + T_points.point_data["T0"] = vis.scalar_fn_to_pv_points(T_points, Ts0) + T_points.point_data["Tp"] = (T_points.point_data["T0"] + T_points.point_data["Ta"])/2 + T_points.point_data["dT"] = T_points.point_data["T"] - T_points.point_data["Ta"] + + pvmesh2 = vis.mesh_to_pv_mesh(mesh) + pvmesh2.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh2, T.sym) + pvmesh2.point_data["T0"] = vis.scalar_fn_to_pv_points(pvmesh2, Ts0) + pvmesh2.points[:,1] += 0.3 + + pvmesh3 = vis.mesh_to_pv_mesh(mesh) + pvmesh3.point_data["Ta"] = vis.scalar_fn_to_pv_points(pvmesh2, TsVKT) + pvmesh3.points[:,1] -= 0.3 + + + pl = pv.Plotter() + + pl.add_mesh( + pvmesh2, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="T", + use_transparency=False, + show_scalar_bar=False, + opacity=1, + ) + + + pl.add_mesh( + + pvmesh3, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="Ta", + use_transparency=False, + show_scalar_bar=False, + opacity=1, + ) + + pl.add_points(T_points, color="White", + scalars="dT", cmap="coolwarm", + point_size=5.0, opacity=0.5) + + + pl.add_arrows(pvmesh.points, pvmesh.point_data["V"], mag=0.00003, opacity=0.5, show_scalar_bar=False) + + # pl.add_points(pdata) + + pl.show(cpos="xy") + + # return vsol + +T_points.point_data["dT"].max() + +T_points.point_data["Ta"].max() + +# +# diff --git a/main/_sources/Notebooks/Examples-Convection/Ex_Convection_1_SLCN_Cartesian.py b/main/_sources/Notebooks/Examples-Convection/Ex_Convection_1_SLCN_Cartesian.py new file mode 100644 index 0000000..0dc7383 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Convection/Ex_Convection_1_SLCN_Cartesian.py @@ -0,0 +1,345 @@ +# # Constant viscosity convection, Cartesian domain (benchmark) +# +# This is a simple example in which we try to instantiate two solvers on the mesh and have them use a common set of variables. +# +# We set up a v, p, T system in which we will solve for a steady-state T field in response to thermal boundary conditions and then use the steady-state T field to compute a stokes flow in response. +# +# The next step is to add particles at node points and sample back along the streamlines to find values of the T field at a previous time. +# +# (Note, we keep all the pieces from previous increments of this problem to ensure that we don't break something along the way) + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import petsc4py +from petsc4py import PETSc + +import underworld3 as uw +from underworld3.systems import Stokes +from underworld3 import function + +import numpy as np +import sympy +# - + +meshbox = uw.meshing.UnstructuredSimplexBox( + minCoords=(0.0, 0.0), + maxCoords=(1.0, 1.0), + cellSize=1.0 / 32.0, + regular=False, + qdegree=3, +) + + +# + +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + # pv.start_xvfb() + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + pl = pv.Plotter(window_size=(1000, 750)) + + # pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.5) + pl.add_mesh(pvmesh, edge_color="Black", show_edges=True) + + pl.show(cpos="xy") +# - + +v_soln = uw.discretisation.MeshVariable("U", meshbox, meshbox.dim, degree=2) +p_soln = uw.discretisation.MeshVariable("P", meshbox, 1, degree=1) +t_soln = uw.discretisation.MeshVariable("T", meshbox, 1, degree=3) +t_0 = uw.discretisation.MeshVariable("T0", meshbox, 1, degree=3) + + +# + +# Create Stokes object + +stokes = Stokes( + meshbox, + velocityField=v_soln, + pressureField=p_soln, + solver_name="stokes", +) + +# Constant viscosity + +viscosity = 1 +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.shear_viscosity_0 = 1.0 +stokes.tolerance = 1.0e-3 + + +# free slip. +# note with petsc we always need to provide a vector of correct cardinality. + +stokes.add_dirichlet_bc((sympy.oo,0.0), "Bottom") +stokes.add_dirichlet_bc((sympy.oo, 0.0), "Top") +stokes.add_dirichlet_bc((0.0,sympy.oo), "Left") +stokes.add_dirichlet_bc((0.0,sympy.oo), "Right") + + +# + +# Create a density structure / buoyancy force +# gravity will vary linearly from zero at the centre +# of the sphere to (say) 1 at the surface + +import sympy + +# Some useful coordinate stuff + +x, y = meshbox.X + + +# + +# Create adv_diff object + +# Set some things +k = 1.0 +h = 0.0 + +adv_diff = uw.systems.AdvDiffusionSLCN( + meshbox, + u_Field=t_soln, + V_fn=v_soln, + solver_name="adv_diff", +) + +adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel +adv_diff.constitutive_model.Parameters.diffusivity = k +adv_diff.theta = 0.5 + + +# + +# Define T boundary conditions via a sympy function + +import sympy + +init_t = 0.01 * sympy.sin(5.0 * x) * sympy.sin(np.pi * y) + (1.0 - y) + +adv_diff.add_dirichlet_bc(1.0, "Bottom") +adv_diff.add_dirichlet_bc(0.0, "Top") + +with meshbox.access(t_0, t_soln): + t_0.data[...] = uw.function.evaluate(init_t, t_0.coords, meshbox.N).reshape(-1, 1) + t_soln.data[...] = t_0.data[...] +# - + + +buoyancy_force = 1.0e6 * t_soln.sym[0] +stokes.bodyforce = sympy.Matrix([0, buoyancy_force]) + +# check the stokes solve is set up and that it converges +stokes.solve(zero_init_guess=True) + +# Check the diffusion part of the solve converges +adv_diff.solve(timestep=0.1 * stokes.estimate_dt(), zero_init_guess=True) + +# + +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)/10 + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + points = vis.meshVariable_to_pv_cloud(t_soln) + point_cloud = pv.PolyData(points) + point_cloud.point_data["Tp"] = vis.scalar_fn_to_pv_points(points, t_soln.sym) + + # points = np.zeros((t_soln.coords.shape[0], 3)) + # points[:, 0] = t_soln.coords[:, 0] + # points[:, 1] = t_soln.coords[:, 1] + + # point_cloud = pv.PolyData(points) + + # with meshbox.access(): + # point_cloud.point_data["Tp"] = t_soln.data.copy() + + # point sources at cell centres + cpoints = np.zeros((meshbox._centroids.shape[0] // 4, 3)) + cpoints[:, 0] = meshbox._centroids[::4, 0] + cpoints[:, 1] = meshbox._centroids[::4, 1] + cpoint_cloud = pv.PolyData(cpoints) + + pvstream = pvmesh.streamlines_from_source( + cpoint_cloud, + vectors="V", + integrator_type=2, + integration_direction="forward", + compute_vorticity=False, + max_steps=1000, + surface_streamlines=True, + ) + + pl = pv.Plotter(window_size=(1000, 750)) + + # pl.add_mesh(pvmesh,'Gray', 'wireframe') + + # pl.add_mesh( + # pvmesh, cmap="coolwarm", edge_color="Black", + # show_edges=True, scalars="T", use_transparency=False, opacity=0.5, + # ) + + pl.add_points( + point_cloud, + cmap="coolwarm", + render_points_as_spheres=True, + point_size=10, + opacity=0.33, + ) + + pl.add_mesh(pvstream, opacity=0.5) + # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1) + + # pl.add_points(pdata) + + pl.show(cpos="xy") + pvmesh.clear_data() + pvmesh.clear_point_data() + + +# - + + +pvmesh.clear_point_data() + + +def plot_T_mesh(filename): + if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)/333 + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + + # point sources at cell centres + cpoints = np.zeros((meshbox._centroids.shape[0] // 4, 3)) + cpoints[:, 0] = meshbox._centroids[::4, 0] + cpoints[:, 1] = meshbox._centroids[::4, 1] + cpoint_cloud = pv.PolyData(cpoints) + + pvstream = pvmesh.streamlines_from_source( + cpoint_cloud, + vectors="V", + integrator_type=45, + integration_direction="forward", + compute_vorticity=False, + max_steps=25, + surface_streamlines=True, + ) + + points = vis.meshVariable_to_pv_cloud(t_soln) + points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym) + point_cloud = pv.PolyData(points) + + pl = pv.Plotter(window_size=(1000, 750)) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Gray", + show_edges=True, + scalars="T", + use_transparency=False, + opacity=0.5, + ) + + pl.add_points( + point_cloud, + cmap="coolwarm", + render_points_as_spheres=False, + point_size=10, + opacity=0.5, + ) + + pl.add_mesh(pvstream, opacity=0.4) + + pl.remove_scalar_bar("T") + pl.remove_scalar_bar("V") + + pl.screenshot( + filename="{}.png".format(filename), + window_size=(1280, 1280), + return_img=False, + ) + # pl.show() + pl.close() + + pvmesh.clear_data() + pvmesh.clear_point_data() + + pv.close_all() + + +t_step = 0 + +# + +# Convection model / update in time + +## +## There is a strange interaction here between the solvers if the zero_guess is +## set to False +## + +expt_name = "output/Ra1e6" + +for step in range(0, 50): + stokes.solve(zero_init_guess=True) + delta_t = 5.0 * stokes.estimate_dt() + adv_diff.solve(timestep=delta_t, zero_init_guess=False) + + # stats then loop + tstats = t_soln.stats() + + if uw.mpi.rank == 0: + print("Timestep {}, dt {}".format(step, delta_t)) + # print(tstats) + + if t_step % 5 == 0: + plot_T_mesh(filename="{}_step_{}".format(expt_name, t_step)) + + t_step += 1 + +# - + + +# savefile = "output_conv/convection_cylinder.h5".format(step) +# meshbox.save(savefile) +# v_soln.save(savefile) +# t_soln.save(savefile) +# meshbox.generate_xdmf(savefile) + + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, stokes.u.sym) + + pl = pv.Plotter(window_size=(1000, 750)) + + # pl.add_arrows(arrow_loc, arrow_length, mag=0.00002, opacity=0.75) + # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1) + + # pl.add_points(point_cloud, cmap="coolwarm", render_points_as_spheres=False, point_size=7.5, opacity=0.25) + + pl.add_mesh(pvmesh, cmap="coolwarm", scalars="T", opacity=0.75) + + pl.show(cpos="xy") + + diff --git a/main/_sources/Notebooks/Examples-Convection/Ex_Convection_2_SLCN_Cartesian-TdepVisc.py b/main/_sources/Notebooks/Examples-Convection/Ex_Convection_2_SLCN_Cartesian-TdepVisc.py new file mode 100644 index 0000000..bb1dd1c --- /dev/null +++ b/main/_sources/Notebooks/Examples-Convection/Ex_Convection_2_SLCN_Cartesian-TdepVisc.py @@ -0,0 +1,280 @@ +# # Temperature-dependent viscosity convection, Cartesian Domain(benchmark) +# +# This is a simple example in which we try to instantiate two solvers on the mesh and have them use a common set of variables. +# +# We set up a v, p, T system in which we will solve for a steady-state T field in response to thermal boundary conditions and then use the steady-state T field to compute a stokes flow in response. +# +# The next step is to add particles at node points and sample back along the streamlines to find values of the T field at a previous time. +# +# (Note, we keep all the pieces from previous increments of this problem to ensure that we don't break something along the way) + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import petsc4py +from petsc4py import PETSc + +import underworld3 as uw +from underworld3.systems import Stokes +from underworld3 import function + +import numpy as np +import sympy + +# - + + +meshbox = uw.meshing.UnstructuredSimplexBox( + minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0), cellSize=1.0 / 32.0, qdegree=3 +) + + +v_soln = uw.discretisation.MeshVariable("U", meshbox, meshbox.dim, degree=2) +p_soln = uw.discretisation.MeshVariable("P", meshbox, 1, degree=1, continuous=True) +t_soln = uw.discretisation.MeshVariable("T", meshbox, 1, degree=3) +t_0 = uw.discretisation.MeshVariable("T0", meshbox, 1, degree=3) + + +# + +# Create Stokes object + +stokes = Stokes( + meshbox, + velocityField=v_soln, + pressureField=p_soln, + solver_name="stokes", +) + +# Set solve options here (or remove default values +# stokes.petsc_options.getAll() +# stokes.petsc_options.delValue("ksp_monitor") +# stokes.petsc_options["snes_test_jacobian"] = None + +# T dependent visc +log10_delta_eta = 6 +delta_eta = 10**log10_delta_eta + +stokes.petsc_options["snes_rtol"] = 1 / delta_eta +stokes.petsc_options["snes_atol"] = 0.01 # Based on how the scaling works + +viscosity = delta_eta * sympy.exp(-sympy.log(delta_eta) * t_soln.sym[0]) +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.viscosity = viscosity +stokes.penalty = 0.0 + +stokes.saddle_preconditioner = 1.0 / viscosity + +# Velocity boundary conditions +stokes.add_dirichlet_bc((0.0,), "Top", (1,)) +stokes.add_dirichlet_bc((0.0,), "Bottom", (1,)) +stokes.add_dirichlet_bc((0.0,), "Left", (0,)) +stokes.add_dirichlet_bc((0.0,), "Right", (0,)) + + +# + +# Create a density structure / buoyancy force +# gravity will vary linearly from zero at the centre +# of the sphere to (say) 1 at the surface + +import sympy + +# Some useful coordinate stuff + +x, y = meshbox.X + + +# + +# Create adv_diff object + +# Set some things +k = 1.0 +h = 0.0 + +adv_diff = uw.systems.AdvDiffusionSLCN( + meshbox, + u_Field=t_soln, + V_fn=v_soln, + solver_name="adv_diff", +) + +adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel +adv_diff.constitutive_model.Parameters.diffusivity = k +adv_diff.theta = 0.5 + + +# + +# Define T boundary conditions via a sympy function + +import sympy + +init_t = 0.9 * (0.05 * sympy.cos(sympy.pi * x) + sympy.cos(0.5 * np.pi * y)) + 0.05 + +adv_diff.add_dirichlet_bc(1.0, "Bottom") +adv_diff.add_dirichlet_bc(0.0, "Top") + +with meshbox.access(t_0, t_soln): + t_0.data[...] = uw.function.evaluate(init_t, t_0.coords).reshape(-1, 1) + t_soln.data[...] = t_0.data[...] + print(t_0.data.max(), t_0.data.min()) +# - + + +buoyancy_force = 1.0e6 * t_soln.sym[0] +stokes.bodyforce = sympy.Matrix([0, buoyancy_force]) + +# check the stokes solve is set up and that it converges +stokes.solve(zero_init_guess=True) + + +# Check the diffusion part of the solve converges +adv_diff.solve(timestep=0.1 * stokes.estimate_dt()) + + +def plot_T_mesh(filename): + if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)/333 + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + + # point sources at cell centres + cpoints = np.zeros((meshbox._centroids[::4, 0].shape[0], 3)) + cpoints[:, 0] = meshbox._centroids[::4, 0] + cpoints[:, 1] = meshbox._centroids[::4, 1] + cpoint_cloud = pv.PolyData(cpoints) + + pvstream = pvmesh.streamlines_from_source( + cpoint_cloud, + vectors="V", + integrator_type=45, + integration_direction="forward", + compute_vorticity=False, + max_steps=25, + surface_streamlines=True, + ) + + points = vis.meshVariable_to_pv_cloud(t_soln) + points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym) + point_cloud = pv.PolyData(points) + + ## PLOTTING + pl = pv.Plotter(window_size=(1000, 750)) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Gray", + show_edges=True, + scalars="T", + use_transparency=False, + opacity=0.5, + ) + + pl.add_points( + point_cloud, + cmap="coolwarm", + render_points_as_spheres=False, + point_size=10, + opacity=0.5, + ) + + pl.add_mesh(pvstream, opacity=0.4) + + pl.remove_scalar_bar("T") + pl.remove_scalar_bar("V") + + pl.screenshot( + filename="{}.png".format(filename), + window_size=(1280, 1280), + return_img=False, + ) + # pl.show() + + pvmesh.clear_data() + pvmesh.clear_point_data() + + +t_step = 0 + +# + +# Convection model / update in time + +## +## There is a strange interaction here between the solvers if the zero_guess is +## set to False +## + +expt_name = f"output/Ra1e6_eta1e{log10_delta_eta}" + +for step in range(0, 1000): + stokes.solve(zero_init_guess=False) + delta_t = 5.0 * stokes.estimate_dt() + adv_diff.solve(timestep=delta_t, zero_init_guess=True) + + # stats then loop + tstats = t_soln.stats() + + if uw.mpi.rank == 0: + print("Timestep {}, dt {}".format(step, delta_t)) + # print(tstats) + + if t_step % 5 == 0: + plot_T_mesh(filename="{}_step_{}".format(expt_name, t_step)) + + t_step += 1 + +# savefile = "{}_ts_{}.h5".format(expt_name,step) +# meshbox.save(savefile) +# v_soln.save(savefile) +# t_soln.save(savefile) +# meshbox.generate_xdmf(savefile) + +pass +# - + + +# savefile = "output_conv/convection_cylinder.h5".format(step) +# meshbox.save(savefile) +# v_soln.save(savefile) +# t_soln.save(savefile) +# meshbox.generate_xdmf(savefile) + + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(stokes.u) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym) + + points = vis.meshVariable_to_pv_cloud(t_soln) + points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym) + point_cloud = pv.PolyData(points) + + pl = pv.Plotter(window_size=(1000, 750)) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.00001, opacity=0.75) + # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1) + + pl.add_points( + point_cloud, + cmap="coolwarm", + render_points_as_spheres=True, + point_size=7, + opacity=0.25, + ) + + pl.add_mesh(pvmesh, "Black", "wireframe", opacity=0.75) + + pl.show(cpos="xy") + + diff --git a/main/_sources/Notebooks/Examples-Convection/Ex_Convection_3_SLCN_Cylindrical-TdepVisc.py b/main/_sources/Notebooks/Examples-Convection/Ex_Convection_3_SLCN_Cylindrical-TdepVisc.py new file mode 100644 index 0000000..a6be9f5 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Convection/Ex_Convection_3_SLCN_Cylindrical-TdepVisc.py @@ -0,0 +1,391 @@ +# # Temperature-dependent viscosity convection, Cylindrical domain (benchmark) +# +# This is a simple example in which we try to instantiate two solvers on the mesh and have them use a common set of variables. +# +# We set up a v, p, T system in which we will solve for a steady-state T field in response to thermal boundary conditions and then use the steady-state T field to compute a stokes flow in response. +# +# The next step is to add particles at node points and sample back along the streamlines to find values of the T field at a previous time. +# +# This has a free-slip lower boundary and a fixed upper boundary (simplifies the null space, and the lid is stagnant anyway) + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import petsc4py +from petsc4py import PETSc + +import underworld3 as uw +from underworld3.systems import Stokes +from underworld3 import function + +import numpy as np +import sympy + + +# + +# Parameters + +r_o = 1.0 +r_i = 0.5 +res = 1 / 24 + +Rayleigh = 1.0e6 / (r_o - r_i) ** 3 + +log10_delta_eta = 4 + + +# + +# Visualisation + +import pyvista as pv + +pv.global_theme.background = "white" +pv.global_theme.window_size = [750, 250] +pv.global_theme.anti_aliasing = "msaa" +pv.global_theme.jupyter_backend = "trame" +pv.global_theme.smooth_shading = True + + +# + +meshdisc = uw.meshing.Annulus( + radiusOuter=r_o, + radiusInner=r_i, + cellSize=float(res), + qdegree=3, +) + +# +meshdisc.vtk("tmp_ann_mesh.vtk") + +# - + + +v_soln = uw.discretisation.MeshVariable("U", meshdisc, meshdisc.dim, degree=2) +p_soln = uw.discretisation.MeshVariable("P", meshdisc, 1, degree=1, continuous=True) +t_soln = uw.discretisation.MeshVariable("T", meshdisc, 1, degree=3) +meshr = uw.discretisation.MeshVariable(r"r", meshdisc, 1, degree=1) + + +# + +radius_fn = sympy.sqrt( + meshdisc.rvec.dot(meshdisc.rvec) +) # normalise by outer radius if not 1.0 +unit_rvec = meshdisc.X / (radius_fn) +gravity_fn = radius_fn + +# Some useful coordinate stuff + +x, y = meshdisc.CoordinateSystem.X +ra, th = meshdisc.CoordinateSystem.xR + +hw = 1000.0 / res +surface_fn_a = sympy.exp(-(((ra - r_o) / r_o) ** 2) * hw) +surface_fn = sympy.exp(-(((meshr.sym[0] - r_o) / r_o) ** 2) * hw) + +base_fn_a = sympy.exp(-(((ra - r_i) / r_o) ** 2) * hw) +base_fn = sympy.exp(-(((meshr.sym[0] - r_i) / r_o) ** 2) * hw) + +free_slip_penalty_upper = v_soln.sym.dot(unit_rvec) * unit_rvec * surface_fn +free_slip_penalty_lower = v_soln.sym.dot(unit_rvec) * unit_rvec * base_fn + +# + +# Create Stokes object + +stokes = Stokes( + meshdisc, + velocityField=v_soln, + pressureField=p_soln, + solver_name="stokes", +) + +# Set solve options here (or remove default values +# stokes.petsc_options.getAll() +# stokes.petsc_options.delValue("ksp_monitor") +# stokes.petsc_options["snes_test_jacobian"] = None + +# T dependent visc +delta_eta = 10**log10_delta_eta + +stokes.petsc_options["snes_rtol"] = 1 / delta_eta + +viscosity = delta_eta * sympy.exp(-sympy.log(delta_eta) * t_soln.sym[0]) +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.viscosity = viscosity +stokes.penalty = 0.0 + +stokes.saddle_preconditioner = 1.0 / viscosity + +# Velocity boundary conditions +stokes.add_dirichlet_bc((0.0, 0.0), "Upper", (0, 1)) +# stokes.add_dirichlet_bc((0.0,0.0), "Lower", (0,1)) + +# Buoyancy force RHS plus free slip surface enforcement +buoyancy_force = Rayleigh * t_soln.sym[0] * unit_rvec * (1.0 - base_fn) +penalty_terms = 10000000 * free_slip_penalty_lower + +stokes.bodyforce = buoyancy_force - penalty_terms + + +# + +# Create adv_diff object + +# Set some things +k = 1.0 +h = 0.0 + +adv_diff = uw.systems.AdvDiffusionSLCN( + meshdisc, + u_Field=t_soln, + V_fn=v_soln, + solver_name="adv_diff", +) + +adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel +adv_diff.constitutive_model.Parameters.diffusivity = k +adv_diff.theta = 0.5 + + +# + +# Define T boundary conditions via a sympy function + +import sympy + +init_t = 0.9 + 0.05 * (sympy.cos(sympy.pi * th / 2)) * sympy.cos( + 0.5 * np.pi * (ra - r_i) / (r_o - r_i) +) + +adv_diff.add_dirichlet_bc(1.0, "Lower") +adv_diff.add_dirichlet_bc(0.0, "Upper") + + +# + +with meshdisc.access(t_soln): + t_soln.data[...] = uw.function.evaluate(init_t, t_soln.coords, meshdisc.N).reshape( + -1, 1 + ) + +with meshdisc.access(meshr): + meshr.data[:, 0] = uw.function.evaluate( + sympy.sqrt(x**2 + y**2), meshdisc.data, meshdisc.N + ) # cf radius_fn which is 0->1 +# - + +# check the stokes solve is set up and that it converges +stokes.solve(zero_init_guess=True) + + +# Check the diffusion part of the solve converges +adv_diff.solve(timestep=0.1 * stokes.estimate_dt()) + +# + +# adv_diff + +# + +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshdisc) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + + points = vis.meshVariable_to_pv_cloud(t_soln) + points.point_data["Tp"] = vis.scalar_fn_to_pv_points(points, t_soln.sym) + point_cloud = pv.PolyData(points) + + # point sources at cell centres + subsample = 2 + cpoints = np.zeros((meshdisc._centroids[::subsample, 0].shape[0], 3)) + cpoints[:, 0] = meshdisc._centroids[::subsample, 0] + cpoints[:, 1] = meshdisc._centroids[::subsample, 1] + cpoint_cloud = pv.PolyData(cpoints) + + pvstream = pvmesh.streamlines_from_source( + cpoint_cloud, + vectors="V", + integrator_type=2, + integration_direction="forward", + compute_vorticity=False, + max_steps=100, + surface_streamlines=True, + ) + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh(pvmesh, "Gray", "wireframe") + + # pl.add_mesh( + # pvmesh, cmap="coolwarm", edge_color="Black", + # show_edges=True, scalars="T", use_transparency=False, opacity=0.5, + # ) + + pl.add_points( + point_cloud, + cmap="coolwarm", + render_points_as_spheres=False, + point_size=3, + opacity=0.33, + ) + + pl.add_mesh(pvstream, opacity=0.5) + pl.add_arrows(pvmesh.points, pvmesh.point_data["V"], mag=1.0e-4) + + # pl.add_points(pdata) + + pl.show(cpos="xy") + + +# - + + +def plot_T_mesh(filename): + if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshdisc) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + + # point sources at cell centres + cpoints = np.zeros((meshdisc._centroids.shape[0], 3)) + cpoints[:, 0] = meshdisc._centroids[:, 0] + cpoints[:, 1] = meshdisc._centroids[:, 1] + cpoint_cloud = pv.PolyData(cpoints) + + pvstream = pvmesh.streamlines_from_source( + cpoint_cloud, + vectors="V", + integrator_type=45, + integration_direction="forward", + compute_vorticity=False, + max_steps=100, + surface_streamlines=True, + ) + + + points = vis.meshVariable_to_pv_cloud(t_soln) + points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym) + point_cloud = pv.PolyData(points) + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Gray", + show_edges=True, + scalars="T", + use_transparency=False, + opacity=0.5, + ) + + pl.add_points( + point_cloud, + cmap="coolwarm", + render_points_as_spheres=False, + point_size=10, + opacity=0.5, + ) + + pl.add_mesh(pvstream, opacity=0.4) + + pl.remove_scalar_bar("T") + pl.remove_scalar_bar("V") + + pl.screenshot( + filename="{}.png".format(filename), + window_size=(1280, 1280), + return_img=False, + ) + # pl.show() + pl.close() + + pvmesh.clear_data() + pvmesh.clear_point_data() + + pv.close_all() + +t_step = 0 + +# + +# Convection model / update in time + +## +## There is a strange interaction here between the solvers if the zero_guess is +## set to False +## + +expt_name = f"output/Ra1e6_cyl_eta1e{log10_delta_eta}" + +for step in range(0, 1000): + stokes.solve(zero_init_guess=True) + delta_t = 2.0 * stokes.estimate_dt() + adv_diff.solve(timestep=delta_t, zero_init_guess=True) + + # stats then loop + tstats = t_soln.stats() + + if uw.mpi.rank == 0: + print("Timestep {}, dt {}".format(step, delta_t)) + # print(tstats) + + if t_step % 5 == 0: + plot_T_mesh(filename="{}_step_{}".format(expt_name, t_step)) + + t_step += 1 + +# savefile = "{}_ts_{}.h5".format(expt_name,step) +# meshdisc.save(savefile) +# v_soln.save(savefile) +# t_soln.save(savefile) +# meshdisc.generate_xdmf(savefile) + +# - + + +# savefile = "output_conv/convection_cylinder.h5".format(step) +# meshdisc.save(savefile) +# v_soln.save(savefile) +# t_soln.save(savefile) +# meshdisc.generate_xdmf(savefile) + + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshdisc) + + velocity_points = vis.meshVariable_to_pv_cloud(stokes.u) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym) + + points = vis.meshVariable_to_pv_cloud(t_soln) + points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym) + point_cloud = pv.PolyData(points) + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=1e-4, opacity=0.75) + # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1) + + pl.add_points( + point_cloud, + cmap="coolwarm", + render_points_as_spheres=True, + point_size=7, + opacity=0.25, + ) + + pl.add_mesh(pvmesh, "Black", "wireframe", opacity=0.75) + + pl.show(cpos="xy") + + diff --git a/main/_sources/Notebooks/Examples-Convection/Ex_Convection_4_SLCN_Cartesian-NL.py b/main/_sources/Notebooks/Examples-Convection/Ex_Convection_4_SLCN_Cartesian-NL.py new file mode 100644 index 0000000..07fbce9 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Convection/Ex_Convection_4_SLCN_Cartesian-NL.py @@ -0,0 +1,363 @@ +# # Non-linear viscosity convection, Cartesian domain (benchmark) +# +# This is a simple example in which we try to instantiate two solvers on the mesh and have them use a common set of variables. +# +# We set up a v, p, T system in which we will solve for a steady-state T field in response to thermal boundary conditions and then use the steady-state T field to compute a stokes flow in response. +# +# The next step is to add particles at node points and sample back along the streamlines to find values of the T field at a previous time. +# +# (Note, we keep all the pieces from previous increments of this problem to ensure that we don't break something along the way) + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import petsc4py +from petsc4py import PETSc + +import underworld3 as uw +from underworld3.systems import Stokes +from underworld3 import function + +import numpy as np +import sympy + +# - + +meshbox = uw.meshing.UnstructuredSimplexBox( + minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0), cellSize=1.0 / 24.0, qdegree=3 +) + +# + +v_soln = uw.discretisation.MeshVariable("U", meshbox, meshbox.dim, degree=2) +p_soln = uw.discretisation.MeshVariable("P", meshbox, 1, degree=1) +t_soln = uw.discretisation.MeshVariable("T", meshbox, 1, degree=3) +t_0 = uw.discretisation.MeshVariable("T0", meshbox, 1, degree=3) + +visc = uw.discretisation.MeshVariable(r"\eta(\dot\varepsilon)", meshbox, 1, degree=1) + + +# + +# Create Stokes object + +stokes = Stokes( + meshbox, + velocityField=v_soln, + pressureField=p_soln, + solver_name="stokes", +) + +# Set solve options here (or remove default values +# stokes.petsc_options.getAll() +# stokes.petsc_options.delValue("ksp_monitor") + +# Linear visc +delta_eta = 1.0e6 + +viscosity_L = delta_eta * sympy.exp(-sympy.log(delta_eta) * t_soln.sym[0]) +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.viscosity = viscosity_L + +stokes.saddle_preconditioner = 1 / stokes.constitutive_model.Parameters.viscosity +stokes.penalty = 0.0 + +# Velocity boundary conditions +stokes.add_dirichlet_bc((0.0,), "Left", (0,)) +stokes.add_dirichlet_bc((0.0,), "Right", (0,)) +stokes.add_dirichlet_bc((0.0,), "Top", (1,)) +stokes.add_dirichlet_bc((0.0,), "Bottom", (1,)) +# - + + +stokes.constitutive_model.solver = stokes + +# + +# isinstance(Stokes, uw.systems.SNES_SaddlePoint) + +# + +# Create a density structure / buoyancy force +# gravity will vary linearly from zero at the centre +# of the sphere to (say) 1 at the surface + +import sympy + +# Some useful coordinate stuff + +x, y = meshbox.X + + +# + +# Create adv_diff object + +# Set some things +k = 1.0 +h = 0.0 + +adv_diff = uw.systems.AdvDiffusion( + meshbox, + u_Field=t_soln, + V_fn=v_soln, + solver_name="adv_diff", +) + +adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel +adv_diff.constitutive_model.Parameters.diffusivity = k + +adv_diff.theta = 0.5 + + +# + +# Create a scalar function calculator that we can use to obtain viscosity etc + +scalar_projection = uw.systems.Projection(meshbox, visc) +scalar_projection.uw_function = 0.1 + 10.0 / (1.0 + stokes.Unknowns.Einv2) +scalar_projection.smoothing = 1.0e-6 + + +# + +expt_name = "output/Ra1e6_NL" + +Rayleigh = 1.0e6 +buoyancy_force = Rayleigh * t_soln.sym[0] +stokes.bodyforce = sympy.Matrix([0, buoyancy_force]) + +# + +# Define T boundary conditions via a sympy function + +import sympy + +init_t = 0.9 * (0.05 * sympy.cos(sympy.pi * x) + sympy.cos(0.5 * np.pi * y)) + 0.05 + +adv_diff.add_dirichlet_bc(1.0, "Bottom") +adv_diff.add_dirichlet_bc(0.0, "Top") + +with meshbox.access(t_0, t_soln): + t_0.data[...] = uw.function.evaluate(init_t, t_0.coords).reshape(-1, 1) + t_soln.data[...] = t_0.data[...] +# - + + +# Linear problem for initial solution of velocity field +stokes.solve() + +# + +# Now make the viscosity non-linear + +viscosity_NL = viscosity_L * (0.1 + 10.0 / (1.0 + stokes.Unknowns.Einv2)) + +stokes.constitutive_model.Parameters.viscosity = viscosity_NL +stokes.saddle_preconditioner = 1 / viscosity_NL +# - + +stokes.saddle_preconditioner + +stokes.solve(zero_init_guess=False) + +# Check the diffusion part of the solve converges +adv_diff.solve(timestep=0.01 * stokes.estimate_dt()) + + +# Compute viscosity field +scalar_projection.solve() + +with meshbox.access(): + print(visc.min(), visc.max()) + +# check the mesh if in a notebook / serial +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + pvmesh.point_data["V"] = 10.0 * vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) / vis.vector_fn_to_pv_points(pvmesh, v_soln.sym).max() + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + pvmesh.point_data["eta"] = vis.scalar_fn_to_pv_points(pvmesh, visc.sym) + + # point sources at cell centres + cpoints = np.zeros((meshbox._centroids[::2].shape[0], 3)) + cpoints[:, 0] = meshbox._centroids[::2, 0] + cpoints[:, 1] = meshbox._centroids[::2, 1] + cpoint_cloud = pv.PolyData(cpoints) + + pvstream = pvmesh.streamlines_from_source( + cpoint_cloud, + vectors="V", + integrator_type=45, + integration_direction="forward", + compute_vorticity=False, + max_steps=25, + surface_streamlines=True, + ) + + points = vis.meshVariable_to_pv_cloud(t_soln) + points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym) + point_cloud = pv.PolyData(points) + + ## PLOTTING + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Gray", + show_edges=True, + scalars="T", + use_transparency=False, + opacity=0.5, + ) + + pl.add_mesh( + pvmesh, + cmap="Greys", + show_edges=False, + scalars="eta", + use_transparency=False, + opacity=0.25, + ) + + # pl.add_points(point_cloud, cmap="coolwarm", render_points_as_spheres=False, point_size=10, opacity=0.5) + + pl.add_mesh(pvstream, opacity=0.4) + + pl.remove_scalar_bar("T") + pl.remove_scalar_bar("V") + pl.remove_scalar_bar("eta") + + # pl.screenshot(filename="{}.png".format(filename), window_size=(1280, 1280), return_img=False) + pl.show() + + +def plot_T_mesh(filename): + if uw.mpi.size == 1: + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + pvmesh.point_data["V"] = 10.0 * vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) / vis.vector_fn_to_pv_points(pvmesh, v_soln.sym).max() + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + pvmesh.point_data["eta"] = vis.scalar_fn_to_pv_points(pvmesh, visc.sym) + + # point sources at cell centres + cpoints = np.zeros((meshbox._centroids[::2].shape[0], 3)) + cpoints[:, 0] = meshbox._centroids[::2, 0] + cpoints[:, 1] = meshbox._centroids[::2, 1] + cpoint_cloud = pv.PolyData(cpoints) + + pvstream = pvmesh.streamlines_from_source( + cpoint_cloud, + vectors="V", + integrator_type=45, + integration_direction="forward", + compute_vorticity=False, + max_steps=25, + surface_streamlines=True, + ) + + points = vis.meshVariable_to_pv_cloud(t_soln) + points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym) + point_cloud = pv.PolyData(points) + + ## PLOTTING + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Gray", + show_edges=True, + scalars="T", + use_transparency=False, + opacity=0.5, + ) + + pl.add_mesh( + pvmesh, + cmap="Greys", + show_edges=False, + scalars="eta", + use_transparency=False, + opacity=0.25, + ) + + # pl.add_points(point_cloud, cmap="coolwarm", render_points_as_spheres=False, point_size=10, opacity=0.5) + + pl.add_mesh(pvstream, opacity=0.4) + + pl.remove_scalar_bar("T") + pl.remove_scalar_bar("V") + pl.remove_scalar_bar("eta") + + pl.screenshot( + filename="{}.png".format(filename), + window_size=(1280, 1280), + return_img=False, + ) + + +# + +# Convection model / update in time + + +for step in range(0, 250): + stokes.solve(zero_init_guess=False) + delta_t = 5.0 * stokes.estimate_dt() + adv_diff.solve(timestep=delta_t, zero_init_guess=False) + + # stats then loop + tstats = t_soln.stats() + + if uw.mpi.rank == 0: + print("Timestep {}, dt {}".format(step, delta_t)) + + plot_T_mesh(filename="{}_step_{}".format(expt_name, step)) + + # savefile = "{}_ts_{}.h5".format(expt_name,step) + # meshbox.save(savefile) + # v_soln.save(savefile) + # t_soln.save(savefile) + # meshbox.generate_xdmf(savefile) + +pass + +# - + + +# savefile = "output_conv/convection_cylinder.h5".format(step) +# meshbox.save(savefile) +# v_soln.save(savefile) +# t_soln.save(savefile) +# meshbox.generate_xdmf(savefile) + + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + velocity_points = vis.meshVariable_to_pv_cloud(stokes.u) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym) + + points = vis.meshVariable_to_pv_cloud(t_soln) + points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym) + point_cloud = pv.PolyData(points) + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.00002, opacity=0.75) + + pl.add_points( + point_cloud, + cmap="coolwarm", + render_points_as_spheres=True, + point_size=7.5, + opacity=0.25, + ) + + pl.add_mesh(pvmesh, "Black", "wireframe", opacity=0.75) + + pl.show(cpos="xy") + + diff --git a/main/_sources/Notebooks/Examples-Convection/Ex_Convection_5_SLCN_Cartesian-Yield.py b/main/_sources/Notebooks/Examples-Convection/Ex_Convection_5_SLCN_Cartesian-Yield.py new file mode 100644 index 0000000..a0df299 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Convection/Ex_Convection_5_SLCN_Cartesian-Yield.py @@ -0,0 +1,375 @@ +# # Non-linear viscosity convection, Cartesian domain (benchmark) +# +# This is a convection example with a yield stress but no strain softening. This can be one of the more challenging problems from a solver point of view because the structure of the non-linear response does not get locked into the solution by localisation. +# +# This example demonstrates that the `sympy.Piecewise` description of the viscosity upon yielding is differentiable and can be used to construct Jacobians. + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import petsc4py +from petsc4py import PETSc + +import underworld3 as uw +from underworld3.systems import Stokes +from underworld3 import function + +import numpy as np +import sympy + +# - + +meshbox = uw.meshing.UnstructuredSimplexBox( + minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0), cellSize=1.0 / 24.0, qdegree=3 +) + +# + +v_soln = uw.discretisation.MeshVariable("U", meshbox, meshbox.dim, degree=2) +p_soln = uw.discretisation.MeshVariable("P", meshbox, 1, degree=1) +t_soln = uw.discretisation.MeshVariable("T", meshbox, 1, degree=3) +t_0 = uw.discretisation.MeshVariable("T0", meshbox, 1, degree=3) + +visc = uw.discretisation.MeshVariable(r"\eta(\dot\varepsilon)", meshbox, 1, degree=2) +tau_inv = uw.discretisation.MeshVariable(r"|\tau|", meshbox, 1, degree=2) + + +# + +# Create Stokes object + +stokes = Stokes( + meshbox, + velocityField=v_soln, + pressureField=p_soln, + solver_name="stokes", +) + +# Set solve options here (or remove default values +# stokes.petsc_options.getAll() +# stokes.petsc_options.delValue("ksp_monitor") +stokes.petsc_options["ksp_monitor"] = None + +# Linear visc +delta_eta = 1.0e6 + +viscosity_L = delta_eta * sympy.exp(-sympy.log(delta_eta) * t_soln.sym[0]) +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.viscosity = viscosity_L +stokes.saddle_preconditioner = 1 / viscosity_L +stokes.penalty = 0.0 + +# Velocity boundary conditions +stokes.add_dirichlet_bc((0.0,), "Left", (0,)) +stokes.add_dirichlet_bc((0.0,), "Right", (0,)) +stokes.add_dirichlet_bc((0.0,), "Top", (1,)) +stokes.add_dirichlet_bc((0.0,), "Bottom", (1,)) + + +# + +# Create a density structure / buoyancy force +# gravity will vary linearly from zero at the centre +# of the sphere to (say) 1 at the surface + +import sympy + +# Some useful coordinate stuff + +x, y = meshbox.X + + +# + +# Create adv_diff object + +# Set some things +k = 1.0 +h = 0.0 + +adv_diff = uw.systems.AdvDiffusion( + meshbox, + u_Field=t_soln, + V_fn=v_soln, + solver_name="adv_diff", +) + +adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel +adv_diff.constitutive_model.Parameters.diffusivity = k + +adv_diff.theta = 0.5 + + +# + +# Create scalar function evaluators that we can use to obtain viscosity / stress + +viscosity_evaluation = uw.systems.Projection(meshbox, visc) +viscosity_evaluation.uw_function = 0.1 + 10.0 / ( + 1.0 + stokes.Unknowns.Einv2 +) # stokes.constitutive_model.material_properties.viscosity +viscosity_evaluation.smoothing = 1.0e-3 +# +stress_inv_evaluation = uw.systems.Projection(meshbox, tau_inv) +stress_inv_evaluation.uw_function = ( + 2.0 * stokes.constitutive_model.Parameters.viscosity * stokes.Unknowns.Einv2 +) +stress_inv_evaluation.smoothing = 1.0e-3 + + +# + +expt_name = "output/Ra1e6_TauY" + +Rayleigh = 1.0e6 +buoyancy_force = Rayleigh * t_soln.sym[0] +stokes.bodyforce = sympy.Matrix([0, buoyancy_force]) + +# + +# Define T boundary conditions via a sympy function + +import sympy + +init_t = 0.9 * (0.05 * sympy.cos(sympy.pi * x) + sympy.cos(0.5 * np.pi * y)) + 0.05 + +adv_diff.add_dirichlet_bc(1.0, "Bottom") +adv_diff.add_dirichlet_bc(0.0, "Top") + +with meshbox.access(t_0, t_soln): + t_0.data[...] = uw.function.evaluate(init_t, t_0.coords).reshape(-1, 1) + t_soln.data[...] = t_0.data[...] +# - + + +# Linear problem for initial solution of velocity field +stokes.solve() + +# + +# Now make the viscosity non-linear + +tau_Y = 1.0e5 * (1 + 100 * (1 - y)) + +viscosity_NL = sympy.Piecewise( + (viscosity_L, 2 * viscosity_L * stokes.Unknowns.Einv2 < tau_Y), + (tau_Y / (2 * stokes.Unknowns.Einv2), True), +) + +stokes.constitutive_model.Parameters.viscosity = viscosity_NL +stokes.saddle_preconditioner = 1 / viscosity_NL +# - + +stokes.solve(zero_init_guess=False) + +# Check the diffusion part of the solve converges +adv_diff.solve(timestep=0.01 * stokes.estimate_dt()) + + +# Compute viscosity field +viscosity_evaluation.solve() +stress_inv_evaluation.solve() + +with meshbox.access(): + print(visc.min(), visc.max()) + print(tau_inv.min(), tau_inv.max()) + + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + pvmesh.point_data["V"] = 1.0 * vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) / vis.vector_fn_to_pv_points(pvmesh, v_soln.sym).max() + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + pvmesh.point_data["eta"] = vis.scalar_fn_to_pv_points(pvmesh, visc.sym) + pvmesh.point_data["tau"] = vis.scalar_fn_to_pv_points(pvmesh, tau_inv.sym) + + # point sources at cell centres + subsample = 10 + cpoints = np.zeros((meshbox._centroids[::subsample].shape[0], 3)) + cpoints[:, 0] = meshbox._centroids[::subsample, 0] + cpoints[:, 1] = meshbox._centroids[::subsample, 1] + cpoint_cloud = pv.PolyData(cpoints) + + pvstream = pvmesh.streamlines_from_source( + cpoint_cloud, + vectors="V", + integrator_type=45, + integration_direction="forward", + compute_vorticity=False, + max_steps=25, + surface_streamlines=True, + ) + + points = vis.meshVariable_to_pv_cloud(t_soln) + points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym) + point_cloud = pv.PolyData(points) + + ## PLOTTING + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Gray", + show_edges=False, + scalars="T", + use_transparency=False, + opacity=0.75, + ) + + pl.add_mesh( + pvmesh, + cmap="Greys", + show_edges=False, + scalars="eta", + use_transparency=False, + opacity="geom", + ) + + # pl.add_points(point_cloud, cmap="coolwarm", render_points_as_spheres=False, point_size=10, opacity=0.5) + + pl.add_mesh(pvstream, opacity=0.2) + + # pl.screenshot(filename="{}.png".format(filename), window_size=(1280, 1280), return_img=False) + pl.show() + + +def plot_T_mesh(filename): + if uw.mpi.size == 1: + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + pvmesh.point_data["V"] = 10.0 * vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) / vis.vector_fn_to_pv_points(pvmesh, v_soln.sym).max() + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + pvmesh.point_data["eta"] = vis.scalar_fn_to_pv_points(pvmesh, visc.sym) + pvmesh.point_data["tau"] = vis.scalar_fn_to_pv_points(pvmesh, tau_inv.sym) + + # point sources at cell centres + subsample = 10 + cpoints = np.zeros((meshbox._centroids[::subsample].shape[0], 3)) + cpoints[:, 0] = meshbox._centroids[::subsample, 0] + cpoints[:, 1] = meshbox._centroids[::subsample, 1] + cpoint_cloud = pv.PolyData(cpoints) + + pvstream = pvmesh.streamlines_from_source( + cpoint_cloud, + vectors="V", + integrator_type=45, + integration_direction="forward", + compute_vorticity=False, + max_steps=25, + surface_streamlines=True, + ) + + points = vis.meshVariable_to_pv_cloud(t_soln) + points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym) + point_cloud = pv.PolyData(points) + + ## PLOTTING + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Gray", + show_edges=True, + scalars="T", + use_transparency=False, + opacity=0.75, + ) + + pl.add_mesh( + pvmesh, + cmap="Greys", + show_edges=False, + scalars="eta", + use_transparency=False, + opacity="geom", + ) + + pl.add_mesh(pvstream, opacity=0.5) + + for key in pvmesh.point_data.keys(): + try: + pl.remove_scalar_bar(key) + except KeyError: + pass + + pl.screenshot( + filename="{}.png".format(filename), + window_size=(1280, 1280), + return_img=False, + ) + + +# + +# Convection model / update in time + + +for step in range(0, 250): + stokes.solve(zero_init_guess=False) + delta_t = 5.0 * stokes.estimate_dt() + adv_diff.solve(timestep=delta_t, zero_init_guess=False) + + # stats then loop + tstats = t_soln.stats() + + if uw.mpi.rank == 0: + print("Timestep {}, dt {}".format(step, delta_t)) + + plot_T_mesh(filename="{}_step_{}".format(expt_name, step)) + + # savefile = "{}_ts_{}.h5".format(expt_name,step) + # meshbox.save(savefile) + # v_soln.save(savefile) + # t_soln.save(savefile) + # meshbox.generate_xdmf(savefile) + +pass + +# - + + +# savefile = "output_conv/convection_cylinder.h5".format(step) +# meshbox.save(savefile) +# v_soln.save(savefile) +# t_soln.save(savefile) +# meshbox.generate_xdmf(savefile) + + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + velocity_points = vis.meshVariable_to_pv_cloud(stokes.u) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym) + + points = vis.meshVariable_to_pv_cloud(t_soln) + points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym) + point_cloud = pv.PolyData(points) + + points = np.zeros((t_soln.coords.shape[0], 3)) + points[:, 0] = t_soln.coords[:, 0] + points[:, 1] = t_soln.coords[:, 1] + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.00002, opacity=0.75) + # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1) + + pl.add_points( + point_cloud, + cmap="coolwarm", + render_points_as_spheres=True, + point_size=7.5, + opacity=0.25, + ) + + pl.add_mesh(pvmesh, "Black", "wireframe", opacity=0.75) + + pl.show(cpos="xy") + + diff --git a/main/_sources/Notebooks/Examples-Convection/Ex_Convection_Cartesian-Swarm.py b/main/_sources/Notebooks/Examples-Convection/Ex_Convection_Cartesian-Swarm.py new file mode 100644 index 0000000..14182d4 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Convection/Ex_Convection_Cartesian-Swarm.py @@ -0,0 +1,322 @@ +# # Constant viscosity convection, Cartesian domain (benchmark) +# +# This is a simple example in which we try to instantiate two solvers on the mesh and have them use a common set of variables. +# +# We set up a v, p, T system in which we will solve for a steady-state T field in response to thermal boundary conditions and then use the steady-state T field to compute a stokes flow in response. +# +# The next step is to add particles at node points and sample back along the streamlines to find values of the T field at a previous time. +# +# (Note, we keep all the pieces from previous increments of this problem to ensure that we don't break something along the way) + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import petsc4py +from petsc4py import PETSc + +import underworld3 as uw +from underworld3.systems import Stokes +from underworld3 import function + +import numpy as np +import sympy + +# - + +meshbox = uw.meshing.UnstructuredSimplexBox( + minCoords=(0.0, 0.0), + maxCoords=(1.0, 1.0), + cellSize=1.0 / 32.0, + regular=True, + ) +meshbox.dm.view() + +# + + +import sympy + +# Some useful coordinate stuff + +x = meshbox.N.x +y = meshbox.N.y +# - + +# check the mesh if in a notebook / serial +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + + pl = pv.Plotter(window_size=(750, 750)) + + # pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.5) + pl.add_mesh(pvmesh, edge_color="Black", show_edges=True) + + pl.show(cpos="xy") + +v_soln = uw.discretisation.MeshVariable("U", meshbox, meshbox.dim, degree=2) +p_soln = uw.discretisation.MeshVariable("P", meshbox, 1, degree=1) +t_soln = uw.discretisation.MeshVariable("T", meshbox, 1, degree=3) +t_0 = uw.discretisation.MeshVariable("T0", meshbox, 1, degree=3) + + +swarm = uw.swarm.Swarm(mesh=meshbox) +T1 = uw.swarm.SwarmVariable("Tminus1", swarm, 1, proxy_degree=3) +swarm.populate(fill_param=5) + + +# + +ad = uw.systems.AdvDiffusion(meshbox, t_soln, T1.sym, order=3) + +ad._u_star_projector.smoothing = 0.0 + +ad.add_dirichlet_bc(1.0, "Bottom") +ad.add_dirichlet_bc(0.0, "Top") + +init_t = 0.01 * sympy.sin(5.0 * x) * sympy.sin(np.pi * y) + (1.0 - y) + +with meshbox.access(t_0, t_soln): + t_0.data[...] = uw.function.evaluate(init_t, t_0.coords).reshape(-1, 1) + t_soln.data[...] = t_0.data[...] + +with swarm.access(T1): + T1.data[...] = uw.function.evaluate( + init_t, swarm.particle_coordinates.data + ).reshape(-1, 1) + +# + +# Create Stokes object + +stokes = Stokes( + meshbox, + velocityField=v_soln, + pressureField=p_soln, + u_degree=v_soln.degree, + p_degree=p_soln.degree, + solver_name="stokes", + verbose=False, + ) + +# Set solve options here (or remove default values +# stokes.petsc_options.getAll() +stokes.petsc_options.delValue("ksp_monitor") + +# Constant visc +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel(meshbox.dim) +stokes.constitutive_model.Parameters.viscosity = 1 + +# Velocity boundary conditions +stokes.add_dirichlet_bc((0.0,), "Left", (0,)) +stokes.add_dirichlet_bc((0.0,), "Right", (0,)) +stokes.add_dirichlet_bc((0.0,), "Top", (1,)) +stokes.add_dirichlet_bc((0.0,), "Bottom", (1,)) + + +# + +buoyancy_force = 1.0e6 * t_soln.fn +stokes.bodyforce = meshbox.N.j * buoyancy_force + +# check the stokes solve is set up and that it converges +stokes.solve() +# - + + +# check the projection +if uw.mpi.size == 1 and ad.projection: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + pvmesh.point_data["T1"] = vis.scalar_fn_to_pv_points(pvmesh, T1.sym) + pvmesh.point_data["mT1"] = vis.scalar_fn_to_pv_points(pvmesh, ad._u_star_projected.sym) + pvmesh.point_data["dT1"] = vis.scalar_fn_to_pv_points(pvmesh, T1.sym) - vis.scalar_fn_to_pv_points(pvmesh, ad._u_star_projected.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(stokes.u) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym) + + pl = pv.Plotter(window_size=(750, 750)) + + # pl.add_mesh(pvmesh,'Black', 'wireframe') + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="dT1", + use_transparency=False, + opacity=0.5, + ) + + # pl.add_arrows(arrow_loc, arrow_length, mag=1.0e-4, opacity=0.5) + # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1) + + # pl.add_points(pdata) + + pl.show(cpos="xy") + + +def plot_T_mesh(filename): + if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + tpoints = vis.meshVariable_to_pv_cloud(t_soln) + tpoints.point_data["T"] = vis.scalar_fn_to_pv_points(tpoints, t_soln.sym) + tpoint_cloud = pv.PolyData(tpoints) + + spoints = vis.swarm_to_pv_cloud(swarm) + swarm_point_cloud = pv.PolyData(spoints) + with swarm.access(): + swarm_point_cloud.point_data["T1"] = T1.data.copy() + + velocity_points = vis.meshVariable_to_pv_cloud(stokes.u) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym) + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.00001, opacity=0.75) + + pl.add_points( + swarm_point_cloud, # cmap="RdYlBu_r", scalars="T1", + color="Black", + render_points_as_spheres=True, + clim=[0.0, 1.0], + point_size=1.0, + opacity=0.5, + ) + + pl.add_points( + point_cloud, + cmap="coolwarm", + scalars="T", + render_points_as_spheres=False, + clim=[0.0, 1.0], + point_size=10.0, + opacity=0.66, + ) + + # pl.add_mesh(pvmesh, cmap="coolwarm", edge_color="Black", + # show_edges=True, scalars="T",clim=[0.0,1.0], + # use_transparency=False, opacity=0.5) + + pl.remove_scalar_bar("T") + # pl.remove_scalar_bar("T1") + + pl.screenshot( + filename="{}.png".format(filename), + window_size=(1250, 1250), + return_img=False, + ) + # pl.show() + pl.close() + + +# + +# Convection model / update in time + +expt_name = "output/Ra1e6_swarm_pnots" + +ad_delta_t = 0.000033 # target + +for step in range(0, 2): #250 + stokes.solve(zero_init_guess=False) + stokes_delta_t = 5.0 * stokes.estimate_dt() + delta_t = stokes_delta_t + + ad.solve(timestep=delta_t, zero_init_guess=True) + + # update swarm / swarm variables + + with swarm.access(T1): + T1.data[:, 0] = uw.function.evaluate(t_soln.fn, swarm.particle_coordinates.data) + + # advect swarm + swarm.advection(v_soln.fn, delta_t) + + tstats = t_soln.stats() + tstarstats = T1._meshVar.stats() + + if uw.mpi.rank == 0: + print("Timestep {}, dt {}".format(step, delta_t)) + print(tstats[2], tstats[3]) + print(tstarstats[2], tstarstats[3]) + + plot_T_mesh(filename="{}_step_{}".format(expt_name, step)) + +# savefile = "{}_ts_{}.h5".format(expt_name,step) +# meshbox.save(savefile) +# v_soln.save(savefile) +# t_soln.save(savefile) +# meshbox.generate_xdmf(savefile) + +# - + + +# savefile = "output_conv/convection_cylinder.h5".format(step) +# meshbox.save(savefile) +# v_soln.save(savefile) +# t_soln.save(savefile) +# meshbox.generate_xdmf(savefile) + + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + tpoints = vis.meshVariable_to_pv_cloud(t_soln) + tpoints.point_data["T"] = vis.scalar_fn_to_pv_points(tpoints, t_soln.sym) + tpoint_cloud = pv.PolyData(tpoints) + + spoints = vis.swarm_to_pv_cloud(swarm) + swarm_point_cloud = pv.PolyData(spoints) + with swarm.access(): + swarm_point_cloud.point_data["T1"] = T1.data.copy() + + velocity_points = vis.meshVariable_to_pv_cloud(stokes.u) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym) + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.00002, opacity=0.75) + # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1) + + # pl.add_points(point_cloud, cmap="coolwarm", + # render_points_as_spheres=True, + # point_size=7.5, opacity=0.25 + # ) + + pl.add_points( + swarm_point_cloud, + cmap="coolwarm", + render_points_as_spheres=True, + point_size=2.5, + opacity=0.5, + clim=[0.0, 1.0], + ) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="T", + use_transparency=False, + opacity=0.5, + clim=[0.0, 1.0], + ) + + pl.show(cpos="xy") + + diff --git a/main/_sources/Notebooks/Examples-Convection/Ex_Convection_Cartesian_ThermoChem.py b/main/_sources/Notebooks/Examples-Convection/Ex_Convection_Cartesian_ThermoChem.py new file mode 100644 index 0000000..734ea45 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Convection/Ex_Convection_Cartesian_ThermoChem.py @@ -0,0 +1,354 @@ +# # Thermochemical convection +# +# We have a thermal convection (advection-diffusion) problem and a material-swarm mediated density variation +# +# + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import petsc4py +from petsc4py import PETSc + +import underworld3 as uw +from underworld3.systems import Stokes +from underworld3 import function + +import numpy as np + +# options = PETSc.Options() +# options["help"] = None +# options["pc_type"] = "svd" +# options["dm_plex_check_all"] = None +# options.getAll() +# - + +meshbox = uw.meshing.UnstructuredSimplexBox( + minCoords=(0.0, 0.0), + maxCoords=(1.0, 1.0), + cellSize=1.0 / 24.0, + regular=False, + ) +meshbox.dm.view() + +# check the mesh if in a notebook / serial +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + + pl = pv.Plotter(window_size=(750, 750)) + + # pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.5) + pl.add_mesh(pvmesh, edge_color="Black", show_edges=True) + + pl.show(cpos="xy") + +v_soln = uw.discretisation.MeshVariable("U", meshbox, meshbox.dim, degree=2) +p_soln = uw.discretisation.MeshVariable("P", meshbox, 1, degree=1) +t_soln = uw.discretisation.MeshVariable("T", meshbox, 1, degree=3) +t_0 = uw.discretisation.MeshVariable("T0", meshbox, 1, degree=3) + + +swarm = uw.swarm.Swarm(mesh=meshbox) +Mat = uw.swarm.SwarmVariable("Material", swarm, 1, proxy_degree=3) +X0 = uw.swarm.SwarmVariable("X0", swarm, meshbox.dim, _proxy=False) +swarm.populate(fill_param=5) + + +# + +# Create Stokes object + +stokes = Stokes( + meshbox, + velocityField=v_soln, + pressureField=p_soln, + solver_name="stokes", + verbose=False, +) + +# Set solve options here (or remove default values +# stokes.petsc_options.getAll() +stokes.petsc_options.delValue("ksp_monitor") + +# Constant visc +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.viscosity = 1 + +# Velocity boundary conditions +stokes.add_dirichlet_bc((0.0,), "Left", (0,)) +stokes.add_dirichlet_bc((0.0,), "Right", (0,)) +stokes.add_dirichlet_bc((0.0,), "Top", (1,)) +stokes.add_dirichlet_bc((0.0,), "Bottom", (1,)) + +# - + + +mMat = uw.discretisation.MeshVariable("mMat", meshbox, 1, degree=2) +projector = uw.systems.solvers.SNES_Projection(meshbox, mMat) +projector.smoothing = 1.0e-3 + +# + +# Create a density structure / buoyancy force +# gravity will vary linearly from zero at the centre +# of the sphere to (say) 1 at the surface + +import sympy + +# Some useful coordinate stuff + +x = meshbox.N.x +y = meshbox.N.y + + +# + +# Create adv_diff object + +# Set some things +k = 1.0 +h = 0.0 +r_i = 0.5 +r_o = 1.0 + +adv_diff = uw.systems.AdvDiffusion( + meshbox, + u_Field=t_soln, + V_fn=v_soln, + solver_name="adv_diff", + order=3, + verbose=False, + ) + +adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel +adv_diff.constitutive_model.Parameters.diffusivity = k +adv_diff.theta = 0.5 + +adv_diff.add_dirichlet_bc(1.0, "Bottom") +adv_diff.add_dirichlet_bc(0.0, "Top") + + +# + +# Define T boundary / initial conditions via a sympy function + +import sympy + +init_t = 0.01 * sympy.sin(5.0 * x) * sympy.sin(np.pi * y) + (1.0 - y) + +with meshbox.access(t_0, t_soln): + t_0.data[...] = uw.function.evaluate(init_t, t_0.coords).reshape(-1, 1) + t_soln.data[...] = t_0.data[...] +# - + + +with swarm.access(Mat): + Mat.data[:, 0] = 0.5 + 0.5 * np.tanh(100.0 * (swarm.data[:, 1] - 0.25)) + +projector.uw_function = Mat.sym +projector.solve() + +# + +expt_name = "output/Ra1e6_Rc5e5" + +# # +ve Rc means heavy chemical component, +# -ve Rc means light chemical component + +# Here we are using the projected mMat field but we +# can switch this out for the particle field +# to show the equivalence + +buoyancy_force = 1.0e6 * t_soln.fn + 5.0e5 * mMat.fn +stokes.bodyforce = meshbox.N.j * buoyancy_force + +# check the stokes solve is set up and that it converges +stokes.solve() +# - + +# Check the diffusion part of the solve converges +adv_diff.solve(timestep=0.01 * stokes.estimate_dt()) + + +# + +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + velocity_points = vis.meshVariable_to_pv_cloud(stokes.u) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym) + + pl = pv.Plotter(window_size=(750, 750)) + + # pl.add_mesh(pvmesh,'Black', 'wireframe') + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="T", + use_transparency=False, + opacity=0.5, + ) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=1.0e-4, opacity=0.5) + # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1) + + # pl.add_points(pdata) + + pl.show(cpos="xy") + + +# - + + +def plot_T_mesh(filename): + if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + pvmesh.point_data["M"] = vis.scalar_fn_to_pv_points(pvmesh, mMat.sym) + + tpoints = vis.meshVariable_to_pv_cloud(t_soln) + tpoints.point_data["T"] = vis.scalar_fn_to_pv_points(tpoints, t_soln.sym) + tpoint_cloud = pv.PolyData(tpoints) + + spoints = vis.swarm_to_pv_cloud(swarm) + swarm_point_cloud = pv.PolyData(spoints) + with swarm.access(): + swarm_point_cloud.point_data["M"] = Mat.data.copy() + + velocity_points = vis.meshVariable_to_pv_cloud(stokes.u) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym) + + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.00001, opacity=0.75) + + # pl.add_points(point_cloud, cmap="gray", + # render_points_as_spheres=False, + # point_size=10, opacity=0.5 + # ) + + pl.add_points( + swarm_point_cloud, + cmap="RdYlBu", + render_points_as_spheres=True, + point_size=7.5, + opacity=1.0, + ) + + pl.add_mesh( + pvmesh, + cmap="gray", + edge_color="Black", + show_edges=True, + scalars="M", + use_transparency=False, + opacity=0.5, + ) + + pl.remove_scalar_bar("M") + pl.remove_scalar_bar("mag") + + pl.screenshot( + filename="{}.png".format(filename), + window_size=(1280, 1280), + return_img=False, + ) + # pl.show() + + +# + +# Convection model / update in time + +for step in range(0, 50): + stokes.solve(zero_init_guess=False) + delta_t = 3.0e-5 # 5.0*stokes.estimate_dt() + adv_diff.solve(timestep=delta_t, zero_init_guess=False) + + # update swarm locations using v_soln + + swarm.advection(v_soln.fn, delta_t, order=2, corrector=True) + # projector.solve(zero_init_guess=False) + + # stats then loop + tstats = t_soln.stats() + + if uw.mpi.rank == 0: + print("Timestep {}, dt {}".format(step, delta_t)) + # print(tstats) + + plot_T_mesh(filename="{}_step_{}".format(expt_name, step)) + + # savefile = "{}_ts_{}.h5".format(expt_name,step) + # meshbox.save(savefile) + # v_soln.save(savefile) + # t_soln.save(savefile) + # meshbox.generate_xdmf(savefile) +# - + + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + pvmesh.point_data["M"] = vis.scalar_fn_to_pv_points(pvmesh, mMat.sym) + + tpoints = vis.meshVariable_to_pv_cloud(t_soln) + tpoints.point_data["T"] = vis.scalar_fn_to_pv_points(tpoints, t_soln.sym) + tpoints.point_data["M"] = vis.scalar_fn_to_pv_points(tpoints, mMat.sym) + tpoint_cloud = pv.PolyData(tpoints) + + spoints = vis.swarm_to_pv_cloud(swarm) + swarm_point_cloud = pv.PolyData(spoints) + with swarm.access(): + swarm_point_cloud.point_data["M"] = Mat.data.copy() + + velocity_points = vis.meshVariable_to_pv_cloud(stokes.u) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym) + + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.75e-5, opacity=0.75) + + + pl.add_points( + swarm_point_cloud, + cmap="RdYlBu", + render_points_as_spheres=True, + point_size=3.0, + opacity=1.0, + ) + + pl.add_mesh( + pvmesh, + cmap="gray", + edge_color="Black", + show_edges=True, + scalars="M", + use_transparency=False, + opacity=0.5, + ) + + # pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.75) + + pl.show(cpos="xy") + + + + diff --git a/main/_sources/Notebooks/Examples-Convection/Ex_Convection_Cylinder-FS.py b/main/_sources/Notebooks/Examples-Convection/Ex_Convection_Cylinder-FS.py new file mode 100644 index 0000000..0a5657f --- /dev/null +++ b/main/_sources/Notebooks/Examples-Convection/Ex_Convection_Cylinder-FS.py @@ -0,0 +1,358 @@ +# # Stokes in an annulus with adv_diff to solve T and back-in-time sampling with particles +# +# This is a simple example in which we try to instantiate two solvers on the mesh and have them use a common set of variables. +# +# We set up a v, p, T system in which we will solve for a steady-state T field in response to thermal boundary conditions and then use the steady-state T field to compute a stokes flow in response. +# +# The next step is to add particles at node points and sample back along the streamlines to find values of the T field at a previous time. +# +# (Note, we keep all the pieces from previous increments of this problem to ensure that we don't break something along the way) + +# + +# to fix trame issue +# import nest_asyncio +# nest_asyncio.apply() + +# + +import petsc4py +from petsc4py import PETSc + +import underworld3 as uw +from underworld3 import function + +import numpy as np + +# + +import os + +rayleigh=1.0e5 + +output_dir = os.path.join("output","Cylinder_FS_Ra1e5_p") +expt_name = "Cylinder_FS" + +os.makedirs(output_dir, exist_ok=True ) + + +viz = True +# - + +meshball = uw.meshing.Annulus( + radiusInner=0.5, radiusOuter=1.0, cellSize=0.05, qdegree=3 +) + + +# check the mesh if in a notebook / serial +if viz and uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + + pl = pv.Plotter(window_size=(750, 750)) + + # pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.5) + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + use_transparency=False, + opacity=0.5, + ) + + pl.show() + +v_soln = uw.discretisation.MeshVariable("U", meshball, meshball.dim, degree=2) +p_soln = uw.discretisation.MeshVariable("P", meshball, 1, degree=1) +t_soln = uw.discretisation.MeshVariable("T", meshball, 1, degree=3) +t_0 = uw.discretisation.MeshVariable("T0", meshball, 1, degree=3) + + +swarm = uw.swarm.Swarm(mesh=meshball) +T1 = uw.swarm.SwarmVariable("Tminus1", swarm, 1) +X1 = uw.swarm.SwarmVariable("Xminus1", swarm, 2) +swarm.populate(fill_param=3) + + +# + +#### Create Stokes object + +stokes = uw.systems.Stokes( + meshball, velocityField=v_soln, pressureField=p_soln, solver_name="stokes" +) + +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.viscosity = 1.0 + +stokes.petsc_options.delValue("ksp_monitor") +stokes.petsc_options.delValue("snes_monitor") + +# Constant visc +stokes.viscosity = 1.0 + +# Velocity boundary conditions +stokes.add_essential_bc((0.0, 0.0), "Lower", (0, 1)) + + +# + +stokes.petsc_options["snes_type"] = "newtonls" +stokes.petsc_options["ksp_type"] = "fgmres" + +# stokes.petsc_options.setValue("fieldsplit_velocity_pc_type", "mg") +stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_type", "kaskade") +stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_cycle_type", "w") + +stokes.petsc_options["fieldsplit_velocity_mg_coarse_pc_type"] = "svd" +stokes.petsc_options[f"fieldsplit_velocity_ksp_type"] = "fcg" +stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_type"] = "chebyshev" +stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_max_it"] = 7 +stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_converged_maxits"] = None + +# gasm is super-fast ... but mg seems to be bulletproof +# gamg is toughest wrt viscosity + +stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "gamg") +stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "additive") +stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v") + +# # mg, multiplicative - very robust ... similar to gamg, additive + +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "mg") +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "multiplicative") +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v") + +# + +# Create a density structure / buoyancy force +# gravity will vary linearly from zero at the centre +# of the sphere to (say) 1 at the surface + +import sympy + +radius_fn = sympy.sqrt( + meshball.X.dot(meshball.X) +) # normalise by outer radius if not 1.0 +unit_rvec = meshball.X / (radius_fn) +gravity_fn = radius_fn + +# Some useful coordinate stuff + +x = meshball.X[0] +y = meshball.X[1] + +r = meshball.CoordinateSystem.R[0] +th = meshball.CoordinateSystem.R[1] + +# + +# Create adv_diff object + +# Set some things +k = 1.0 +h = 0.0 +r_i = 0.5 +r_o = 1.0 + +adv_diff = uw.systems.AdvDiffusionSLCN( + meshball, + u_Field=t_soln, + V_fn=v_soln, + solver_name="adv_diff", + verbose=False, +) + +adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel +adv_diff.constitutive_model.Parameters.diffusivity = 1 + +adv_diff.tolerance=1.0e-4 + + + +# + +# Define T boundary conditions via a sympy function + +import sympy + +abs_r = sympy.sqrt(meshball.rvec.dot(meshball.rvec)) +init_t = 0.01 * sympy.sin(5.0 * th) * sympy.sin(np.pi * (r - r_i) / (r_o - r_i)) + ( + r_o - r +) / (r_o - r_i) + +adv_diff.add_dirichlet_bc(1.0, "Lower") +adv_diff.add_dirichlet_bc(0.0, "Upper") + +with meshball.access(t_0, t_soln): + t_0.data[...] = uw.function.evaluate(init_t, t_0.coords).reshape(-1, 1) + t_soln.data[...] = t_0.data[...] +# + +buoyancy_force = rayleigh * t_soln.sym[0] / (0.5) ** 3 +stokes.bodyforce = unit_rvec * buoyancy_force + +stokes.tolerance = 1.0e-3 +stokes.petsc_options.setValue("ksp_monitor", None) +stokes.petsc_options.setValue("snes_monitor", None) + +stokes.add_essential_bc([0.0, 0.0], "Lower") # no slip on the base +stokes.add_natural_bc( + 10000 * unit_rvec.dot(v_soln.sym) * unit_rvec.T, "Upper" +) + +stokes.solve(verbose=False, zero_init_guess=True, picard=1 ) + + +# + +# Check the diffusion part of the solve converges +# adv_diff.petsc_options["ksp_monitor"] = None +adv_diff.petsc_options["snes_monitor"] = None + +adv_diff.solve(verbose=False, timestep=0.5 * stokes.estimate_dt()) +# - + + +# check the mesh if in a notebook / serial +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) - vis.scalar_fn_to_pv_points(pvmesh, t_0.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(stokes.u) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym) + + pl = pv.Plotter(window_size=(750, 750)) + + # pl.add_mesh(pvmesh,'Black', 'wireframe') + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="T", + use_transparency=False, + opacity=0.5, + ) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=1000.0/rayleigh, opacity=0.75) + + # pl.add_points(pdata) + + pl.show(cpos="xy") + + +def plot_T_mesh(filename): + if viz and uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + + tpoints = vis.meshVariable_to_pv_cloud(t_soln) + tpoints.point_data["T"] = vis.scalar_fn_to_pv_points(tpoints, t_soln.sym) + tpoint_cloud = pv.PolyData(tpoints) + + velocity_points = vis.meshVariable_to_pv_cloud(stokes.u) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym) + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=10.0/rayleigh, opacity=0.75) + + pl.add_points( + tpoint_cloud, + cmap="coolwarm", + render_points_as_spheres=False, + point_size=10, + opacity=0.66, + ) + + pl.add_mesh(pvmesh, scalars="T", cmap="coolwarm", show_edges=True, opacity=0.75) + + pl.remove_scalar_bar("T") + pl.remove_scalar_bar("mag") + + pl.screenshot( + filename="{}.png".format(filename), + window_size=(1280, 1280), + return_img=False, + ) + # pl.show() + + +ts = 0 + +# + +# Convection model / update in time + + +for step in range(0, 51): + + stokes.solve(verbose=False, zero_init_guess=False, picard=0) + + delta_t = adv_diff.estimate_dt(v_factor=2.0) + adv_diff.solve(timestep=delta_t, zero_init_guess=False) + + stats = t_soln.stats() + stats_star = adv_diff.DuDt.psi_star[0].stats() + + if uw.mpi.rank == 0: + print("Timestep {}, dt {}".format(ts, delta_t)) + print(stats) + print(stats_star) + + if step%5 == 0: + plot_T_mesh(filename="{}_step_{}".format(os.path.join(output_dir, expt_name),ts)) + + if step%10 == 0: + + meshball.write_timestep( + expt_name, + meshUpdates=True, + meshVars=[p_soln, v_soln, t_soln], + outputPath=output_dir, + index=ts, + ) + + ts += 1 + + + +# - + + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + + points = vis.meshVariable_to_pv_cloud(t_soln) + points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym) + point_cloud = pv.PolyData(points) + + velocity_points = vis.meshVariable_to_pv_cloud(stokes.u) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym) + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.00005, opacity=0.75) + # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1) + + pl.add_points( + point_cloud, + cmap="coolwarm", + render_points_as_spheres=True, + point_size=7.5, + opacity=0.75, + ) + + pl.add_mesh(pvmesh, scalars="T", cmap="coolwarm", opacity=1) + + pl.show(cpos="xy") + + diff --git a/main/_sources/Notebooks/Examples-Convection/Ex_Convection_Cylinder.py b/main/_sources/Notebooks/Examples-Convection/Ex_Convection_Cylinder.py new file mode 100644 index 0000000..43919fc --- /dev/null +++ b/main/_sources/Notebooks/Examples-Convection/Ex_Convection_Cylinder.py @@ -0,0 +1,361 @@ +# # Stokes in a disc with adv_diff to solve T and back-in-time sampling with particles +# +# This is a simple example in which we try to instantiate two solvers on the mesh and have them use a common set of variables. +# +# We set up a v, p, T system in which we will solve for a steady-state T field in response to thermal boundary conditions and then use the steady-state T field to compute a stokes flow in response. +# +# The next step is to add particles at node points and sample back along the streamlines to find values of the T field at a previous time. +# +# (Note, we keep all the pieces from previous increments of this problem to ensure that we don't break something along the way) + +# + +# to fix trame issue +# import nest_asyncio +# nest_asyncio.apply() + +# + +import petsc4py +from petsc4py import PETSc + +import underworld3 as uw +from underworld3.systems import Stokes +from underworld3 import function + +import numpy as np +# - + +meshball = uw.meshing.Annulus( + radiusInner=0.5, radiusOuter=1.0, cellSize=0.1, degree=1, qdegree=3 +) + + +# check the mesh if in a notebook / serial +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + + pl = pv.Plotter(window_size=(750, 750)) + + # pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.5) + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + use_transparency=False, + opacity=0.5, + ) + + pl.show() + +v_soln = uw.discretisation.MeshVariable("U", meshball, meshball.dim, degree=2) +p_soln = uw.discretisation.MeshVariable("P", meshball, 1, degree=1) +t_soln = uw.discretisation.MeshVariable("T", meshball, 1, degree=3) +t_0 = uw.discretisation.MeshVariable("T0", meshball, 1, degree=3) + + +swarm = uw.swarm.Swarm(mesh=meshball) +T1 = uw.swarm.SwarmVariable("Tminus1", swarm, 1) +X1 = uw.swarm.SwarmVariable("Xminus1", swarm, 2) +swarm.populate(fill_param=3) + + + + + + +# + +# Create Stokes object + +stokes = Stokes( + meshball, velocityField=v_soln, pressureField=p_soln, solver_name="stokes" +) + +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.viscosity = 1.0 + + +# Set solve options here (or remove default values +# stokes.petsc_options.getAll() +stokes.petsc_options.delValue("ksp_monitor") + +# Constant visc +stokes.viscosity = 1.0 + +# Velocity boundary conditions +stokes.add_dirichlet_bc((0.0, 0.0), "Upper", (0, 1)) +stokes.add_dirichlet_bc((0.0, 0.0), "Lower", (0, 1)) + +# + +# Create a density structure / buoyancy force +# gravity will vary linearly from zero at the centre +# of the sphere to (say) 1 at the surface + +import sympy + +radius_fn = sympy.sqrt( + meshball.X.dot(meshball.X) +) # normalise by outer radius if not 1.0 +unit_rvec = meshball.X / (1.0e-10 + radius_fn) +gravity_fn = radius_fn + +# Some useful coordinate stuff + +x = meshball.X[0] +y = meshball.X[1] + +r = sympy.sqrt(x**2 + y**2) +th = sympy.atan2(y + 1.0e-5, x + 1.0e-5) + +# + +# Create adv_diff object + +# Set some things +k = 1.0 +h = 0.0 +r_i = 0.5 +r_o = 1.0 + +adv_diff = uw.systems.AdvDiffusion( + meshball, + u_Field=t_soln, + V_fn=v_soln, + solver_name="adv_diff", + verbose=False, +) + +adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel +adv_diff.constitutive_model.Parameters.diffusivity = 1 + +adv_diff.theta = 0.5 +# adv_diff.f = t_soln.fn / delta_t - t_star.fn / delta_t + + +# + +# Define T boundary conditions via a sympy function + +import sympy + +abs_r = sympy.sqrt(meshball.rvec.dot(meshball.rvec)) +init_t = 0.01 * sympy.sin(15.0 * th) * sympy.sin(np.pi * (r - r_i) / (r_o - r_i)) + ( + r_o - r +) / (r_o - r_i) + +adv_diff.add_dirichlet_bc(1.0, "Lower") +adv_diff.add_dirichlet_bc(0.0, "Upper") + +with meshball.access(t_0, t_soln): + t_0.data[...] = uw.function.evaluate(init_t, t_0.coords).reshape(-1, 1) + t_soln.data[...] = t_0.data[...] +# + +buoyancy_force = 1.0e6 * t_soln.sym[0] / (0.5) ** 3 +stokes.bodyforce = unit_rvec * buoyancy_force + +# check the stokes solve converges +stokes.solve() + +# + +# Check the diffusion part of the solve converges +adv_diff.petsc_options["ksp_monitor"] = None +adv_diff.petsc_options["monitor"] = None + +adv_diff.solve(timestep=0.00001 * stokes.estimate_dt()) + + +# + +# diff = uw.systems.Poisson(meshball, u_Field=t_soln, solver_name="diff_only") + +# diff.constitutive_model = uw.constitutive_models.DiffusionModel(meshball.dim) +# diff.constitutive_model.material_properties = adv_diff.constitutive_model.Parameters(diffusivity=1) +# diff.solve() + + +# + +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + # pvmesh.point_data["Ts"] = vis.scalar_fn_to_pv_points(pvmesh, adv_diff._u_star.sym) + # pvmesh.point_data["dT"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) - vis.scalar_fn_to_pv_points(pvmesh, adv_diff._u_star.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(stokes.u) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym) + + + pl = pv.Plotter(window_size=(750, 750)) + + # pl.add_mesh(pvmesh,'Black', 'wireframe') + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="T", + use_transparency=False, + opacity=0.5, + ) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.0005) + # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1) + + # pl.add_points(pdata) + + pl.show(cpos="xy") + + +# + +# pvmesh.point_data["Ts"].min() +# - + +adv_diff.petsc_options["pc_gamg_agg_nsmooths"] = 1 + +# check the mesh if in a notebook / serial +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) - vis.scalar_fn_to_pv_points(pvmesh, t_0.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(stokes.u) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym) + + pl = pv.Plotter(window_size=(750, 750)) + + # pl.add_mesh(pvmesh,'Black', 'wireframe') + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="T", + use_transparency=False, + opacity=0.5, + ) + + # pl.add_arrows(arrow_loc, arrow_length, mag=0.025) + + # pl.add_points(pdata) + + pl.show(cpos="xy") + + +def plot_T_mesh(filename): + if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + + tpoints = vis.meshVariable_to_pv_cloud(t_soln) + tpoints.point_data["T"] = vis.scalar_fn_to_pv_points(tpoints, t_soln.sym) + tpoint_cloud = pv.PolyData(tpoints) + + velocity_points = vis.meshVariable_to_pv_cloud(stokes.u) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym) + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.00002, opacity=0.75) + + pl.add_points( + point_cloud, + cmap="coolwarm", + render_points_as_spheres=False, + point_size=10, + opacity=0.66, + ) + + pl.add_mesh(pvmesh, "Black", "wireframe", opacity=0.75) + + pl.remove_scalar_bar("T") + pl.remove_scalar_bar("mag") + + pl.screenshot( + filename="{}.png".format(filename), + window_size=(1280, 1280), + return_img=False, + ) + # pl.show() + + +# + +# Convection model / update in time + +expt_name = "output/Cylinder_Ra1e6i" + +for step in range(0, 50): + stokes.solve() + delta_t = 5.0 * stokes.estimate_dt() + adv_diff.solve(timestep=delta_t) + + # stats then loop + tstats = t_soln.stats() + + if uw.mpi.rank == 0: + print("Timestep {}, dt {}".format(step, delta_t)) + # print(tstats) + + # plot_T_mesh(filename="{}_step_{}".format(expt_name,step)) + + meshball.petsc_save_checkpoint(index=step, + meshVars=[v_soln, t_soln], + outputPath=expt_name) + + + +# + +# savefile = "output_conv/convection_cylinder.h5".format(step) +# meshball.save(savefile) +# v_soln.save(savefile) +# t_soln.save(savefile) +# meshball.generate_xdmf(savefile) +# - + + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + + points = vis.meshVariable_to_pv_cloud(t_soln) + points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym) + point_cloud = pv.PolyData(points) + + velocity_points = vis.meshVariable_to_pv_cloud(stokes.u) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym) + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.00005, opacity=0.75) + # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1) + + pl.add_points( + point_cloud, + cmap="coolwarm", + render_points_as_spheres=True, + point_size=7.5, + opacity=0.75, + ) + + pl.add_mesh(pvmesh, scalars="T", cmap="coolwarm", opacity=1) + + pl.show(cpos="xy") + + diff --git a/main/_sources/Notebooks/Examples-Convection/Ex_Convection_Disc_InternalHeat.py b/main/_sources/Notebooks/Examples-Convection/Ex_Convection_Disc_InternalHeat.py new file mode 100644 index 0000000..42201c7 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Convection/Ex_Convection_Disc_InternalHeat.py @@ -0,0 +1,365 @@ +# # Convection in a disc with internal heating and rigid or free boundaries +# +# + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + + +import underworld3 as uw +from underworld3.systems import Stokes +from underworld3 import function +import os + +import numpy as np +import sympy + +import petsc4py +from petsc4py import PETSc + +# + +## Command line parameters use -uw_resolution 0.1, for example + +res = uw.options.getReal("resolution", default=0.1) +Free_Slip = uw.options.getBool("free_slip", default=True) +restart_step = uw.options.getInt("restart_step", default=0) +max_steps = uw.options.getInt("max_steps", default=1) +delta_eta = uw.options.getReal("delta_eta", default=1000.0) + +viz = True + +# - + +uw.options.view() + +# + +Rayleigh = 1.0e7 +H_int = 1.0 +k = 1.0 +resI = res * 3 +r_o = 1.0 +r_i = 0.0 + + +# For now, assume restart is from same location ! +expt_name = f"Disc_Ra1e7_H1_deleta_{delta_eta}" +output_dir = "output" + +os.makedirs(output_dir, exist_ok=True ) + +# - + +meshball = uw.meshing.AnnulusWithSpokes(radiusOuter=r_o, radiusInner=r_i, + cellSizeOuter=res, + cellSizeInner=resI, + qdegree=3, ) + + +meshball.dm.view() + +# + + +radius_fn = sympy.sqrt(meshball.X.dot(meshball.X)) # normalise by r_o if required +unit_rvec = meshball.X / radius_fn +gravity_fn = radius_fn + +# Some useful coordinate stuff + +x = meshball.N.x +y = meshball.N.y + +r = sympy.sqrt(x**2 + y**2) # cf radius_fn which is 0->1 +th = sympy.atan2(y + 1.0e-5, x + 1.0e-5) + +# - + + +# check the mesh if in a notebook / serial +if viz and uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + + pl = pv.Plotter(window_size=(750, 750)) + pl.add_mesh(pvmesh, cmap="coolwarm", edge_color="Black", show_edges=True, use_transparency=False, opacity=0.5) + + pl.show() + + + +v_soln = uw.discretisation.MeshVariable("U", meshball, meshball.dim, degree=2) +p_soln = uw.discretisation.MeshVariable("P", meshball, 1, degree=1) +t_soln = uw.discretisation.MeshVariable("T", meshball, 1, degree=3) +t_0 = uw.discretisation.MeshVariable("T0", meshball, 1, degree=3) +r_mesh = uw.discretisation.MeshVariable("r", meshball, 1, degree=1) +kappa = uw.discretisation.MeshVariable("kappa", meshball, 1, degree=3, varsymbol=r"\kappa") + +# + +## F-K viscosity function + +C = sympy.log(delta_eta) +viscosity_fn = delta_eta * sympy.exp(-C * 0) + + +# + +# Create Stokes object + +stokes = Stokes(meshball, velocityField=v_soln, pressureField=p_soln, + solver_name="stokes", + verbose=False) + +# Constant viscosity +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.shear_viscosity_0 = viscosity_fn + +# Set solve options here (or remove default values +stokes.tolerance = 1.0e-6 +# stokes.petsc_options.delValue("ksp_monitor") + +stokes.petsc_options.setValue("ksp_monitor", None) +stokes.petsc_options.setValue("snes_monitor", None) + +# Velocity boundary conditions + +if Free_Slip: + GammaN = meshball.Gamma # boundary_normals["Upper"].value + # bc = sympy.Piecewise((1.0, r > 0.99 * r_o), (0.0, True)) + stokes.add_natural_bc( + 1.0e6 * GammaN.dot(v_soln.sym) * GammaN.T, "Upper" + ) + +else: + stokes.add_dirichlet_bc((0.0, 0.0), "Upper") + +# - + + +meshball.Gamma + +# + +# Create adv_diff object + +adv_diff = uw.systems.AdvDiffusionSLCN( + meshball, + u_Field=t_soln, + V_fn=v_soln, + solver_name="adv_diff", + verbose=False, + order=2, +) + +adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel + +## Flux limiting diffusivity (stabilizing term) + +Tgrad = meshball.vector.gradient(t_soln.sym) +Tslope = sympy.sqrt(Tgrad.dot(Tgrad)) +Tslope_max = 25 + +k_lim = (Tslope/Tslope_max) +k_eff = k * sympy.Max(1, k_lim) + +adv_diff.constitutive_model.Parameters.diffusivity = k +adv_diff.f = H_int + + + +# + +## Projection to compute the diffusivity + +calculate_diffusivity = uw.systems.Projection(meshball, u_Field=kappa) +calculate_diffusivity.uw_function = k_eff + + +# + +# Define T boundary conditions via a sympy function + +import sympy + +abs_r = sympy.sqrt(meshball.rvec.dot(meshball.rvec)) +init_t = 0.25 + 0.25 * sympy.sin(7.0 * th) * sympy.sin(np.pi * (r - r_i) / (r_o - r_i)) + 0.0 * (r_o - r) / (r_o - r_i) + +adv_diff.add_dirichlet_bc(0.0, "Upper") + +with meshball.access(t_0, t_soln): + t_0.data[...] = uw.function.evalf(init_t, t_0.coords).reshape(-1, 1) + t_soln.data[...] = t_0.data[...] +# + +# If restart, then pull T from there + +if restart_step != 0: + t_soln.read_timestep(expt_name, "T", restart_step, outputPath=output_dir, verbose=True) + +# - + +with meshball.access(r_mesh): + r_mesh.data[:, 0] = uw.function.evalf(r, meshball.data) + +stokes.bodyforce = unit_rvec * gravity_fn * Rayleigh * t_soln.fn +stokes.solve(verbose=False) + +# + +# Check the diffusion part of the solve converges + +dt = 0.00001 +adv_diff.solve(timestep=dt) +adv_diff.constitutive_model.Parameters.diffusivity = k_eff +adv_diff.solve(timestep=dt, zero_init_guess=False) + +# - + +calculate_diffusivity.solve() + +# + +# check the mesh if in a notebook / serial +if viz and uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym[0]) + pvmesh.point_data["K"] = vis.scalar_fn_to_pv_points(pvmesh, kappa.sym[0]) + + velocity_points = vis.meshVariable_to_pv_cloud(stokes.u) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym) + + pl = pv.Plotter(window_size=(750, 750)) + + # pl.add_mesh(pvmesh,'Black', 'wireframe') + + pl.add_mesh( + pvmesh, cmap="coolwarm", edge_color="Black", + show_edges=True, scalars="T", + use_transparency=False, opacity=1.0, + # clim=[0,1], + ) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.01) + # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1) + + # pl.add_points(pdata) + + pl.show(cpos="xy") + +def plot_T_mesh(filename): + + if viz and uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + + points = vis.meshVariable_to_pv_cloud(t_soln) + points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym) + point_cloud = pv.PolyData(points) + + velocity_points = vis.meshVariable_to_pv_cloud(stokes.u) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym) + + pl = pv.Plotter(window_size=(750, 750)) + + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=50 / Rayleigh) + pl.add_mesh(pvmesh, cmap="coolwarm", + show_edges=True, + scalars="T", opacity=0.75) + + pl.add_points(point_cloud, cmap="coolwarm", + render_points_as_spheres=False, + # clim=[0,1], + point_size=10, opacity=0.66) + + + # pl.remove_scalar_bar("T") + pl.remove_scalar_bar("mag") + + pl.screenshot(filename="{}.png".format(filename), window_size=(1280, 1280), return_img=False) + # pl.show() +# - + +ts = restart_step + +# + +# Convection model / update in time + +delta_t = 5.0e-5 + +for step in range(0, max_steps ): # + + stokes.solve(verbose=False, zero_init_guess=False) + + calculate_diffusivity.solve() + + if step%10 == 0: + delta_t = adv_diff.estimate_dt(v_factor=2.0, diffusivity=kappa.sym[0]) + + adv_diff.solve(timestep=delta_t, zero_init_guess=False ) + + # stats, dt (all collective) print if rank 0, then loop + tstats = t_soln.stats() + Tgrad_stats = kappa.stats() + dt_estimate = adv_diff.estimate_dt(v_factor=2.0, diffusivity=kappa.sym[0]) + + if uw.mpi.rank == 0: + print("Timestep {}, dt {} ({})".format(ts, delta_t, dt_estimate), flush=True) + # print(tstats) + # print("-----") + # print(Tgrad_stats) + # print("=====\n") + + # print(tstats_star) + + if ts % 10 == 0: + plot_T_mesh(filename="output/{}_step_{}".format(expt_name, ts)) + + meshball.write_timestep( + expt_name, + meshUpdates=True, + meshVars=[p_soln, v_soln, t_soln], + outputPath=output_dir, + index=ts, + ) + + ts += 1 + +# - + + +if viz and uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + + points = vis.meshVariable_to_pv_cloud(t_soln) + points.point_data["T"] = vis.scalar_fn_to_pv_points(points, t_soln.sym) + point_cloud = pv.PolyData(points) + + velocity_points = vis.meshVariable_to_pv_cloud(stokes.u) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, stokes.u.sym) + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.01, opacity=0.75) + # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1) + + pl.add_points(point_cloud, cmap="coolwarm", + render_points_as_spheres=True, + point_size=7.5, opacity=0.25) + + pl.add_mesh(pvmesh, cmap="coolwarm", scalars="T", opacity=0.75) + + pl.show(cpos="xy") + + + + diff --git a/main/_sources/Notebooks/Examples-Convection/Readme.md b/main/_sources/Notebooks/Examples-Convection/Readme.md new file mode 100644 index 0000000..d25726f --- /dev/null +++ b/main/_sources/Notebooks/Examples-Convection/Readme.md @@ -0,0 +1,18 @@ +# Convection modelling examples + +Including tests of the advection diffusion solver + +## Recent solver and visualization updates + +- [x] Ex_AdvectionDiffusionSLCN_RotationTest.py +- [x] Ex_AdvectionDiffusionSwarm_RotationTest.py +- [x] Ex_Convection_1_SLCN_Cartesian.py +- [x] Ex_Convection_2_SLCN_Cartesian-TdepVisc.py +- [x] Ex_Convection_3_SLCN_Cylindrical-TdepVisc.py +- [x] Ex_Convection_4_SLCN_Cartesian-NL.py +- [x] Ex_Convection_5_SLCN_Cartesian-Yield.py +- [x] Ex_Convection_Cartesian_ThermoChem.py +- [ ] Ex_Convection_Cartesian-Swarm.py + - [ ] Marked as deprecated. Require updates around advDiff call +- [x] Ex_Convection_Cylinder.py +- [x] Ex_Convection_Disc_InternalHeat.py diff --git a/main/_sources/Notebooks/Examples-Convection/output/README.md b/main/_sources/Notebooks/Examples-Convection/output/README.md new file mode 100644 index 0000000..148e620 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Convection/output/README.md @@ -0,0 +1,3 @@ +# Stokes model outputs + +These files are NOT under version control \ No newline at end of file diff --git a/main/_sources/Notebooks/Examples-Meshing/Manifold_S2.5-SphCoords.py b/main/_sources/Notebooks/Examples-Meshing/Manifold_S2.5-SphCoords.py new file mode 100644 index 0000000..5c93a51 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Meshing/Manifold_S2.5-SphCoords.py @@ -0,0 +1,196 @@ +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import petsc4py +from petsc4py import PETSc + +import underworld3 as uw +import numpy as np +import sympy +import meshio +import gmsh + +import os + +os.environ["SYMPY_USE_CACHE"] = "no" + +# - + + +bubblemesh = uw.meshing.SegmentedSphere( + radiusOuter=1.0, + radiusInner=0.5, + cellSize=0.1, + numSegments=6, + qdegree=3, + filename="tmpWedge.msh", + coordinatesNative=True, +) + +bubblemesh.dm.view() + +# ## Equation systems +# +# We have the surface mesh, two dimensional coordinates in lat, lon, and an appropriate coordinate system definition. We can now define variables etc. +# +# Note, the Cartesian coords are 3D, the lat / lon are 2D +# + +V = uw.discretisation.MeshVariable("U", bubblemesh, 3, degree=2) +P = uw.discretisation.MeshVariable("P", bubblemesh, 1, degree=1, continuous=False) +Pc = uw.discretisation.MeshVariable("Pc",bubblemesh, 1, degree=2) +T = uw.discretisation.MeshVariable("T", bubblemesh, 1, degree=2) +Tnode = uw.discretisation.MeshVariable("Tn", bubblemesh, 1, degree=1) +Tdiff = uw.discretisation.MeshVariable("dT", bubblemesh, 1, degree=1, continuous=False) +Tdiffc = uw.discretisation.MeshVariable("dTc", bubblemesh, 1, degree=1, continuous=True) + +r, lon,lat = bubblemesh.CoordinateSystem.R + +# + + +t_init = sympy.sympify(sympy.sin(6 * lon) * sympy.cos(5 * lat) * sympy.cos(lat)) + +with bubblemesh.access(T, Tnode): + T.data[:,0] = uw.function.evaluate(t_init, T.coords, bubblemesh.N) +# - + +T.gradient() + +slope = sympy.sqrt(T.gradient().dot(T.gradient())) +slope + +T.gradient() + +# ### SNES example +# +# The simplest possible solver implementation is just a projection operator + +# + +projector = uw.systems.Projection(bubblemesh, Tnode) +projector.uw_function = T.sym[0] +projector.smoothing = 1.0e-6 + +# Test if bc's work +projector.add_dirichlet_bc(10.0, "PoleAxisN", 0) +projector.add_dirichlet_bc(10.0, "PolePtNo", 0) +projector.add_dirichlet_bc(10.0, "PolePtNi", 0) +projector.add_dirichlet_bc(-10.0, "PoleAxisS", 0) +projector.add_dirichlet_bc(-10.0, "PolePtSo", 0) +projector.add_dirichlet_bc(-10.0, "PolePtSi", 0) + +options = projector.petsc_options +options.setValue("snes_rtol",1.0e-4) +# options.setValue("pc_gamg_type","agg") +# options.setValue("pc_gamg_agg_nsmooths", 1) +# options.setValue("pc_gamg_threshold", 0.25) + +projector.solve() + +# + +projector = uw.systems.Projection(bubblemesh, Tdiff) +projector.uw_function = slope # sympy.diff(T.sym[0], lon) +projector.smoothing = 1.0e-6 + +projector.add_dirichlet_bc(0.0, "PoleAxisN", 0) +projector.add_dirichlet_bc(0.0, "PolePtNo", 0) +projector.add_dirichlet_bc(0.0, "PolePtNi", 0) +projector.add_dirichlet_bc(0.0, "PoleAxisS", 0) +projector.add_dirichlet_bc(0.0, "PolePtSo", 0) +projector.add_dirichlet_bc(0.0, "PolePtSi", 0) + +options = projector.petsc_options +options.setValue("snes_rtol",1.0e-4) + +projector.solve() +# + +projector = uw.systems.Projection(bubblemesh, Tdiffc) +projector.uw_function = Tdiff.sym[0] # sympy.diff(T.sym[0], lon) +projector.smoothing = 1.0e-6 + +projector.add_dirichlet_bc(0.0, "PoleAxisN", 0) +projector.add_dirichlet_bc(0.0, "PolePtNo", 0) +projector.add_dirichlet_bc(0.0, "PolePtNi", 0) +projector.add_dirichlet_bc(0.0, "PoleAxisS", 0) +projector.add_dirichlet_bc(0.0, "PolePtSo", 0) +projector.add_dirichlet_bc(0.0, "PolePtSi", 0) + +options = projector.petsc_options +options.setValue("snes_rtol",1.0e-4) + +projector.solve() +# - + + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + # pvmesh = vis.mesh_to_pv_mesh(bubblemesh) + pvmesh = pv.read("tmpWedge.msh") + pvmesh.point_data["nT"] = vis.scalar_fn_to_pv_points(pvmesh, Tnode.sym) + pvmesh.point_data["dT"] = vis.scalar_fn_to_pv_points(pvmesh, Tdiff.sym) + pvmesh.point_data["dTc"] = vis.scalar_fn_to_pv_points(pvmesh, Tdiffc.sym) + +pl = pv.Plotter(window_size=(750, 750)) +clipped = pvmesh.clip(normal='x', crinkle=True) +pl.add_mesh( + clipped, + show_edges=True, + scalars="dT", + cmap="RdYlBu", + opacity=1.0, + # clim=[-6,6] + ) +pl.add_axes(labels_off=False) +pl.show(cpos="xy") + +# + +## Below is some useful stuff for dmplex that we should put somewhere +# How to get element edges etc. + +# + +# pstart, pend = dmplex.getDepthStratum(0) +# estart, eend = dmplex.getDepthStratum(1) +# cstart, cend = dmplex.getDepthStratum(2) + +# dmlonlat = dmplex.getCoordinates().array.reshape(-1,2) + +# del_th_max = 0.0 +# for i in range(estart, eend): +# p1 , p2 = dmplex.getCone(i) - (pstart, pstart) +# del_th = np.abs(dmlonlat[p1, 0] - dmlonlat[p2,0]) / np.pi +# del_th_max = max(del_th, del_th_max) +# if np.sign(dmlonlat[p1, 0]) != np.sign(dmlonlat[p2, 0]) : +# if del_th > 1: +# print(f"(dt-> {del_th:.3f} ", +# f"{dmlonlat[p1, 0]:+4.2f} <-> {dmlonlat[p2, 0]:+4.2f} ", +# f"{xyz[p1,2]}" +# ) + +# del_th + +# + +# for cell in range(cstart, cend): +# points = set() +# edges = dmplex.getCone(cell) +# for edge in edges: +# points = points.union(dmplex.getCone(edge)-pstart) + +# print( f"{cell}", +# f"{dmlonlat[tuple(points),0]/np.pi}, {xyz[tuple(points),2]}" +# ) + + +# + +# Some dmplex options + +# https://petsc.org/release/src/dm/impls/plex/plexgmsh.c.html + +# options = PETSc.Options() +# options["dm_plex_gmsh_multiple_tags"] = None +# options["dm_plex_gmsh_spacedim"] = 2 +# options["dm_plex_gmsh_use_regions"] = None +# options["dm_plex_gmsh_mark_vertices"] = None diff --git a/main/_sources/Notebooks/Examples-Meshing/Manifold_S2.5.py b/main/_sources/Notebooks/Examples-Meshing/Manifold_S2.5.py new file mode 100644 index 0000000..9cb24ea --- /dev/null +++ b/main/_sources/Notebooks/Examples-Meshing/Manifold_S2.5.py @@ -0,0 +1,189 @@ +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import petsc4py +from petsc4py import PETSc + +import underworld3 as uw +import numpy as np +import sympy +import meshio +import gmsh + +import os + +os.environ["SYMPY_USE_CACHE"] = "no" + + +# - + + +bubblemesh = uw.meshing.SegmentedSphere( + radiusOuter=1.0, + radiusInner=0.98, + cellSize=0.1, + numSegments=6, + qdegree=3, + filename="tmpWedge.msh", + coordinatesNative=False, +) + + +bubblemesh.dm.view() + +# ## Equation systems +# +# We have the surface mesh, two dimensional coordinates in lat, lon, and an appropriate coordinate system definition. We can now define variables etc. +# +# Note, the Cartesian coords are 3D, the lat / lon are 2D +# + +V = uw.discretisation.MeshVariable("U", bubblemesh, 3, degree=2) +P = uw.discretisation.MeshVariable("P", bubblemesh, 1, degree=1, continuous=False) +Pc = uw.discretisation.MeshVariable("Pc",bubblemesh, 1, degree=2) +T = uw.discretisation.MeshVariable("T", bubblemesh, 1, degree=3) +Tnode = uw.discretisation.MeshVariable("Tn", bubblemesh, 1, degree=1) +Tdiff = uw.discretisation.MeshVariable("dT", bubblemesh, 1, degree=1, continuous=False) +Tdiffc = uw.discretisation.MeshVariable("dTc", bubblemesh, 1, degree=2, continuous=True) + +r, lon,lat = bubblemesh.CoordinateSystem.R + +# + +t_init = sympy.sympify(sympy.sin(6 * lon) * sympy.cos(5 * lat) * sympy.cos(lat)) + +with bubblemesh.access(T): + T.data[:,0] = uw.function.evaluate(t_init, T.coords, bubblemesh.N) +# - + +slope = sympy.sqrt(T.gradient().dot(T.gradient())) +slope + +# ### SNES example +# +# The simplest possible solver implementation is just a projection operator + +# + +projector = uw.systems.Projection(bubblemesh, Tnode) +projector.uw_function = T.sym[0] +projector.smoothing = 1.0e-3 + +options = projector.petsc_options +options.setValue("snes_rtol",1.0e-4) +# options.setValue("pc_gamg_type","agg") +# options.setValue("pc_gamg_agg_nsmooths", 1) +# options.setValue("pc_gamg_threshold", 0.25) + +projector.solve() + +# + +projector = uw.systems.Projection(bubblemesh, Tdiff) +projector.uw_function = slope # sympy.diff(T.sym[0], lon) +projector.smoothing = 1.0e-6 + +projector.add_dirichlet_bc(0.0, "PoleAxisN", 0) +projector.add_dirichlet_bc(0.0, "PolePtNo", 0) +projector.add_dirichlet_bc(0.0, "PolePtNi", 0) +projector.add_dirichlet_bc(0.0, "PoleAxisS", 0) +projector.add_dirichlet_bc(0.0, "PolePtSo", 0) +projector.add_dirichlet_bc(0.0, "PolePtSi", 0) + +options = projector.petsc_options +options.setValue("snes_rtol",1.0e-4) + +projector.solve() +# + +projector = uw.systems.Projection(bubblemesh, Tdiffc) +projector.uw_function = slope # sympy.diff(T.sym[0], lon) +projector.smoothing = 1.0e-6 + +projector.add_dirichlet_bc(0.0, "PoleAxisN", 0) +projector.add_dirichlet_bc(0.0, "PolePtNo", 0) +projector.add_dirichlet_bc(0.0, "PolePtNi", 0) +projector.add_dirichlet_bc(0.0, "PoleAxisS", 0) +projector.add_dirichlet_bc(0.0, "PolePtSo", 0) +projector.add_dirichlet_bc(0.0, "PolePtSi", 0) + +options = projector.petsc_options +options.setValue("snes_rtol",1.0e-4) + + +projector.solve() +# - + + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(bubblemesh) + pvmesh.point_data["nT"] = vis.scalar_fn_to_pv_points(pvmesh, Tnode.sym) + pvmesh.point_data["dT"] = vis.scalar_fn_to_pv_points(pvmesh, Tdiff.sym) + pvmesh.point_data["dTc"] = vis.scalar_fn_to_pv_points(pvmesh, Tdiffc.sym) + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh( + pvmesh, + show_edges=True, + scalars="dT", + cmap="RdYlBu", + opacity=1.0, + # clim=[0,1] + ) + + + pl.add_axes(labels_off=False) + + + pl.show(cpos="xy") + +# + +## Below is some useful stuff for dmplex that we should put somewhere +# How to get element edges etc. + +# + +# pstart, pend = dmplex.getDepthStratum(0) +# estart, eend = dmplex.getDepthStratum(1) +# cstart, cend = dmplex.getDepthStratum(2) + +# dmlonlat = dmplex.getCoordinates().array.reshape(-1,2) + +# del_th_max = 0.0 +# for i in range(estart, eend): +# p1 , p2 = dmplex.getCone(i) - (pstart, pstart) +# del_th = np.abs(dmlonlat[p1, 0] - dmlonlat[p2,0]) / np.pi +# del_th_max = max(del_th, del_th_max) +# if np.sign(dmlonlat[p1, 0]) != np.sign(dmlonlat[p2, 0]) : +# if del_th > 1: +# print(f"(dt-> {del_th:.3f} ", +# f"{dmlonlat[p1, 0]:+4.2f} <-> {dmlonlat[p2, 0]:+4.2f} ", +# f"{xyz[p1,2]}" +# ) + +# del_th + +# + +# for cell in range(cstart, cend): +# points = set() +# edges = dmplex.getCone(cell) +# for edge in edges: +# points = points.union(dmplex.getCone(edge)-pstart) + +# print( f"{cell}", +# f"{dmlonlat[tuple(points),0]/np.pi}, {xyz[tuple(points),2]}" +# ) + + +# + +# Some dmplex options + +# https://petsc.org/release/src/dm/impls/plex/plexgmsh.c.html + +# options = PETSc.Options() +# options["dm_plex_gmsh_multiple_tags"] = None +# options["dm_plex_gmsh_spacedim"] = 2 +# options["dm_plex_gmsh_use_regions"] = None +# options["dm_plex_gmsh_mark_vertices"] = None diff --git a/main/_sources/Notebooks/Examples-Meshing/Manifold_S2.py b/main/_sources/Notebooks/Examples-Meshing/Manifold_S2.py new file mode 100644 index 0000000..26485a4 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Meshing/Manifold_S2.py @@ -0,0 +1,212 @@ +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import petsc4py +from petsc4py import PETSc + +import underworld3 as uw +import numpy as np +import sympy +import meshio +import gmsh + +import os + +os.environ["SYMPY_USE_CACHE"] = "no" + +r_o = 1.0 +# - + + +bubblemesh = uw.meshing.SegmentedSphericalSurface2D(cellSize=0.05,numSegments=3, qdegree=3, filename="tmpManifold.msh") + +bubblemesh.dm.view() + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + # pvmesh = vis.mesh_to_pv_mesh(bubblemesh) + pvmesh = pv.read("tmpManifold.msh") + pvmesh.point_data["lon"] = bubblemesh.data[:,0] + pvmesh.point_data["lat"] = bubblemesh.data[:,1] + + pl = pv.Plotter() + + pl.add_mesh( + pvmesh, + show_edges=True, + scalars="lon", + + ) + + pl.show(cpos="xy") + +# ## Equation systems +# +# We have the surface mesh, two dimensional coordinates in lat, lon, and an appropriate coordinate system definition. We can now define variables etc. +# +# Note, the Cartesian coords are 3D, the lat / lon are 2D +# + +T = uw.discretisation.MeshVariable("T", bubblemesh, 1, degree=3) +Tnode = uw.discretisation.MeshVariable("Tn", bubblemesh, 1, degree=1) +Tdiff = uw.discretisation.MeshVariable("dT", bubblemesh, 1, degree=1, continuous=False) +Tdiffc = uw.discretisation.MeshVariable("dTc", bubblemesh, 1, degree=1, continuous=True) + +lon,lat = bubblemesh.CoordinateSystem.N + +# + +# Don't use the function.evaluate as is seems to break + +with bubblemesh.access(T): + T.data[:,0] = np.sin(T.coords[:,0]*6) * np.cos(5*T.coords[:,1]) * np.cos(T.coords[:,1]) +# - + +slope = sympy.sqrt(T.gradient().dot(T.gradient())) +slope + +# ### SNES example +# +# The simplest possible solver implementation is just a projection operator + +# + +projector = uw.systems.Projection(bubblemesh, Tnode) +projector.uw_function = T.sym[0] +projector.smoothing = 1.0e-3 + +options = projector.petsc_options +options.setValue("snes_rtol",1.0e-4) +options.setValue("pc_gamg_type","agg") +options.setValue("pc_gamg_agg_nsmooths", 1) +options.setValue("pc_gamg_threshold", 0.25) + +projector.solve() + +# + +projector = uw.systems.Projection(bubblemesh, Tdiff) +projector.uw_function = slope +projector.smoothing = 1.0e-6 +projector.add_dirichlet_bc((0.0,), "Poles", (0,) ) + +options = projector.petsc_options +options.setValue("snes_rtol",1.0e-6) + +projector.solve() +# + +projector = uw.systems.Projection(bubblemesh, Tdiffc) +projector.uw_function = Tdiff.sym[0] +projector.smoothing = 1.0e-6 +projector.add_dirichlet_bc((0.0,), "Poles", (0,) ) + +options = projector.petsc_options +options.setValue("snes_rtol",1.0e-6) + +projector.solve() +# - + + + + +# + +# if uw.mpi.size == 1: + +# import pyvista as pv +# import underworld3.visualisation as vis + +# # pvmesh = pv.read("tmpManifold.msh") +# pvmesh = vis.mesh_to_pv_mesh("tmpManifold.msh") +# pvmesh.point_data["nT"] = vis.scalar_fn_to_pv_points(pvmesh, Tnode.sym) +# pvmesh.point_data["dT"] = vis.scalar_fn_to_pv_points(pvmesh, Tdiff.sym) +# pvmesh.point_data["dTc"] = vis.scalar_fn_to_pv_points(pvmesh, Tdiffc.sym) + +if uw.mpi.size == 1: + import numpy as np + import pyvista as pv + import vtk + + pv.global_theme.background = "white" + pv.global_theme.window_size = [1050, 500] + pv.global_theme.antialiasing = True + pv.global_theme.jupyter_backend = "trame" + pv.global_theme.smooth_shading = True + pv.global_theme.camera["viewup"] = [0.0, 1.0, 0.0] + pv.global_theme.camera["position"] = [0.0, 0.0, 1.0] + + pvmesh = pv.read("tmpManifold.msh") + + with bubblemesh.access(): + pvmesh.point_data["nT"] = Tnode.data[:,0] + pvmesh.point_data["dT"] = uw.function.evaluate(Tdiff.sym[0], bubblemesh.data) + pvmesh.point_data["dTc"] = Tdiffc.data[:,0] + +# + +pl = pv.Plotter(window_size=(750, 750)) + +pl.add_mesh( + pvmesh, + show_edges=True, + scalars="dT", + cmap="RdYlBu", + opacity=1, +) + + +pl.add_axes(labels_off=False) + + +pl.show(cpos="xy") +# - + +slope + +# + +## Below is some useful stuff for dmplex that we should put somewhere +# How to get element edges etc. + +# + +# pstart, pend = dmplex.getDepthStratum(0) +# estart, eend = dmplex.getDepthStratum(1) +# cstart, cend = dmplex.getDepthStratum(2) + +# dmlonlat = dmplex.getCoordinates().array.reshape(-1,2) + +# del_th_max = 0.0 +# for i in range(estart, eend): +# p1 , p2 = dmplex.getCone(i) - (pstart, pstart) +# del_th = np.abs(dmlonlat[p1, 0] - dmlonlat[p2,0]) / np.pi +# del_th_max = max(del_th, del_th_max) +# if np.sign(dmlonlat[p1, 0]) != np.sign(dmlonlat[p2, 0]) : +# if del_th > 1: +# print(f"(dt-> {del_th:.3f} ", +# f"{dmlonlat[p1, 0]:+4.2f} <-> {dmlonlat[p2, 0]:+4.2f} ", +# f"{xyz[p1,2]}" +# ) + +# del_th + +# + +# for cell in range(cstart, cend): +# points = set() +# edges = dmplex.getCone(cell) +# for edge in edges: +# points = points.union(dmplex.getCone(edge)-pstart) + +# print( f"{cell}", +# f"{dmlonlat[tuple(points),0]/np.pi}, {xyz[tuple(points),2]}" +# ) + + +# + +# Some dmplex options + +# https://petsc.org/release/src/dm/impls/plex/plexgmsh.c.html + +# options = PETSc.Options() +# options["dm_plex_gmsh_multiple_tags"] = None +# options["dm_plex_gmsh_spacedim"] = 2 +# options["dm_plex_gmsh_use_regions"] = None +# options["dm_plex_gmsh_mark_vertices"] = None diff --git a/main/_sources/Notebooks/Examples-Meshing/MeshVariablePoints.py b/main/_sources/Notebooks/Examples-Meshing/MeshVariablePoints.py new file mode 100644 index 0000000..a96b0b9 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Meshing/MeshVariablePoints.py @@ -0,0 +1,121 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# + [markdown] magic_args="[markdown]" +# # Creating a mesh (and checking the labels) +# +# This example is for the notch-localization test of Spiegelman et al. For which they supply a geometry file which gmsh can use to construct meshes at various resolutions. NOTE: we are just demonstrating the mesh here, not the solver configuration / benchmarking. +# +# The `.geo` file is provided and we show how to make this into a `.msh` file and +# how to read that into a `uw.discretisation.Mesh` object. The `.geo` file has header parameters to control the mesh refinement, and we provide a coarse version and the original version. +# +# After that, there is some cell data which we can assign to a data structure on the elements (such as a swarm). +# - + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +import petsc4py +from petsc4py import PETSc +import underworld3 as uw +import numpy as np +import sympy + +from underworld3.cython import petsc_discretisation + + +# %% +mesh1 = uw.meshing.Annulus(radiusInner=0.5, radiusOuter=1.0, cellSize=0.1) +mesh1.dm.view() + + +# + +# %% +# petsc_discretisation.petsc_dm_get_periodicity(mesh1.dm) +# - + +# %% +petsc_discretisation.petsc_dm_set_periodicity( + mesh1.dm, (0.5, 3.14159, 0.0), (0.0, 0.0, 0.0), (1.0, 6.28, 0.0) +) + +# %% +coodm = mesh1.dm.getCoordinateDM() +coodm.view() +mesh1.dm.localizeCoordinates() + +# %% +mesh1.dm.view() + +# + +# %% +# petsc_discretisation.petsc_dm_get_periodicity(mesh1.dm) +# - + +# %% +dC0 = uw.discretisation.MeshVariable(r"dC_0", mesh1, 1, degree=0, continuous=False) +dC1 = uw.discretisation.MeshVariable(r"dC_1", mesh1, 1, degree=1, continuous=False) +dC2 = uw.discretisation.MeshVariable(r"dC_2", mesh1, 1, degree=2, continuous=False) +C1 = uw.discretisation.MeshVariable(r"C_1", mesh1, 1, degree=1, continuous=True) +C2 = uw.discretisation.MeshVariable(r"C_2", mesh1, 1, degree=2, continuous=True) + + +# + [markdown] magic_args="[markdown]" +# This is how we extract cell data from the mesh. We can map it to the swarm data structure +# - + +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh1) + + points = vis.meshVariable_to_pv_cloud(dC1) + point_cloud = pv.PolyData(points) + + points = vis.meshVariable_to_pv_cloud(dC2) + point_cloud2 = pv.PolyData(points) + + pl = pv.Plotter() + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + use_transparency=False, + opacity=0.5, + ) + pl.add_points( + point_cloud, + color="Red", + render_points_as_spheres=True, + point_size=5, + opacity=0.66, + ) + pl.add_points( + point_cloud2, + color="Blue", + render_points_as_spheres=True, + point_size=5, + opacity=0.66, + ) + + pl.show(cpos="xy") + + diff --git a/main/_sources/Notebooks/Examples-Meshing/output/README.md b/main/_sources/Notebooks/Examples-Meshing/output/README.md new file mode 100644 index 0000000..02b6fd0 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Meshing/output/README.md @@ -0,0 +1,3 @@ +# Meshing model outputs + +These files are NOT under version control \ No newline at end of file diff --git a/main/_sources/Notebooks/Examples-NavierStokes/Ex_NavierStokesRotationTest.py b/main/_sources/Notebooks/Examples-NavierStokes/Ex_NavierStokesRotationTest.py new file mode 100644 index 0000000..713920c --- /dev/null +++ b/main/_sources/Notebooks/Examples-NavierStokes/Ex_NavierStokesRotationTest.py @@ -0,0 +1,365 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Navier Stokes test: boundary driven ring with step change in boundary conditions +# +# This should develop a boundary layer with sqrt(t) growth rate + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + + + +# + +import petsc4py +from petsc4py import PETSc + +import underworld3 as uw +from underworld3.systems import Stokes +from underworld3.systems import NavierStokesSLCN + +from underworld3 import function + +import numpy as np +import sympy + + +# + +# Parameters that define the notebook +# These can be set when launching the script as +# mpirun python3 scriptname -uw_resolution=0.1 etc + +resolution = uw.options.getInt("model_resolution", default=25) +refinement = uw.options.getInt("model_refinement", default=0) +maxsteps = uw.options.getInt("max_steps", default=1000) +restart_step = uw.options.getInt("restart_step", default=-1) +rho = uw.options.getReal("rho", default=1000) +# - + +meshball = uw.meshing.Annulus( + radiusOuter=1.0, radiusInner=0.5, cellSize=1/resolution, qdegree=3 +) + + +# + +# Define some functions on the mesh + +import sympy + +radius_fn = sympy.sqrt( + meshball.rvec.dot(meshball.rvec) +) # normalise by outer radius if not 1.0 +unit_rvec = meshball.rvec / (1.0e-10 + radius_fn) + +# Some useful coordinate stuff + +x = meshball.N.x +y = meshball.N.y + +r = sympy.sqrt(x**2 + y**2) +th = sympy.atan2(y + 1.0e-5, x + 1.0e-5) + +# Rigid body rotation v_theta = constant, v_r = 0.0 + +theta_dot = 2.0 * np.pi # i.e one revolution in time 1.0 +v_x = -1.0 * r * theta_dot * sympy.sin(th) # * y # to make a convergent / divergent bc +v_y = r * theta_dot * sympy.cos(th) # * y +# - + +v_soln = uw.discretisation.MeshVariable("U", meshball, meshball.dim, degree=2) +p_soln = uw.discretisation.MeshVariable("P", meshball, 1, degree=1) +vorticity = uw.discretisation.MeshVariable( + "\omega", meshball, 1, degree=1, continuous=False +) + + +navier_stokes = NavierStokesSLCN( + meshball, + velocityField=v_soln, + pressureField=p_soln, + rho=rho, + solver_name="navier_stokes", + order=2, +) + +# + +navier_stokes.petsc_options["snes_monitor"] = None +navier_stokes.petsc_options["ksp_monitor"] = None + +navier_stokes.petsc_options["snes_type"] = "newtonls" +navier_stokes.petsc_options["ksp_type"] = "fgmres" + +navier_stokes.petsc_options.setValue("fieldsplit_velocity_pc_type", "mg") +navier_stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_type", "kaskade") +navier_stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_cycle_type", "w") + +navier_stokes.petsc_options["fieldsplit_velocity_mg_coarse_pc_type"] = "svd" +navier_stokes.petsc_options["fieldsplit_velocity_ksp_type"] = "fcg" +navier_stokes.petsc_options["fieldsplit_velocity_mg_levels_ksp_type"] = "chebyshev" +navier_stokes.petsc_options["fieldsplit_velocity_mg_levels_ksp_max_it"] = 2 +navier_stokes.petsc_options["fieldsplit_velocity_mg_levels_ksp_converged_maxits"] = None + +# mg, multiplicative - very robust ... similar to gamg, additive + +navier_stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "mg") +navier_stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "multiplicative") +navier_stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v") +# - + + +nodal_vorticity_from_v = uw.systems.Projection(meshball, vorticity) +nodal_vorticity_from_v.uw_function = meshball.vector.curl(v_soln.sym) +nodal_vorticity_from_v.smoothing = 0.0 + + +# + +passive_swarm = uw.swarm.Swarm(mesh=meshball) +passive_swarm.populate( + fill_param=3, +) + +# add new points at the 12 o'clock position + +npoints = 100 +passive_swarm.dm.addNPoints(npoints) +with passive_swarm.access(passive_swarm.particle_coordinates): + for i in range(npoints): + passive_swarm.particle_coordinates.data[-1 : -(npoints + 1) : -1, :] = np.array( + [-0.05, 0.9] + 0.1 * np.random.random((npoints, 2)) + ) + + +# + +# Constant visc + +navier_stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +navier_stokes.constitutive_model.Parameters.viscosity = 1.0 + +# Constant visc + +navier_stokes.penalty = 0.1 +navier_stokes.bodyforce = sympy.Matrix([0, 0]) + +# Velocity boundary conditions +navier_stokes.add_dirichlet_bc((v_x, v_y), "Upper", (0, 1)) +navier_stokes.add_dirichlet_bc((0.0, 0.0), "Lower", (0, 1)) + +expt_name = f"Cylinder_NS_rho_{navier_stokes.rho}_{resolution}" + +# - + +navier_stokes.solve(timestep=0.1) +navier_stokes.estimate_dt() + +nodal_vorticity_from_v.solve() + + +# check the mesh if in a notebook / serial +if uw.mpi.size == 1: + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + pvmesh.point_data["Omega"] = vis.scalar_fn_to_pv_points(pvmesh, vorticity.sym) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points( + velocity_points, v_soln.sym + ) + + passive_swarm_points = uw.visualisation.swarm_to_pv_cloud(passive_swarm) + + + # point sources at cell centres + points = np.zeros((meshball._centroids.shape[0], 3)) + points[:, 0] = meshball._centroids[:, 0] + points[:, 1] = meshball._centroids[:, 1] + centroid_cloud = pv.PolyData(points) + + pvstream = pvmesh.streamlines_from_source( + centroid_cloud, + vectors="V", + integration_direction="both", + surface_streamlines=True, + max_time=0.25, + ) + + pl = pv.Plotter(window_size=(1000, 750)) + + pl.add_mesh(pvmesh, cmap="RdBu", scalars="Omega", opacity=0.5, show_edges=True) + pl.add_mesh(pvstream, opacity=0.33) + pl.add_arrows( + velocity_points.points, + velocity_points.point_data["V"], + mag=1.0e-2, + opacity=0.75, + ) + + pl.add_points( + passive_swarm_points, + color="Black", + render_points_as_spheres=True, + point_size=3, + opacity=0.5, + ) + + pl.camera.SetPosition(0.75, 0.2, 1.5) + pl.camera.SetFocalPoint(0.75, 0.2, 0.0) + pl.camera.SetClippingRange(1.0, 8.0) + + # pl.remove_scalar_bar("Omega") + pl.remove_scalar_bar("mag") + pl.remove_scalar_bar("V") + + pl.show(jupyter_backend="client") + + +def plot_V_mesh(filename): + if uw.mpi.size == 1: + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym) + pvmesh.point_data["Omega"] = vis.scalar_fn_to_pv_points(pvmesh, vorticity.sym) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points( + velocity_points, v_soln.sym + ) + + passive_swarm_points = uw.visualisation.swarm_to_pv_cloud(passive_swarm) + + # point sources at cell centres + points = np.zeros((meshball._centroids.shape[0], 3)) + points[:, 0] = meshball._centroids[:, 0] + points[:, 1] = meshball._centroids[:, 1] + centroid_cloud = pv.PolyData(points) + + pvstream = pvmesh.streamlines_from_source( + centroid_cloud, + vectors="V", + integration_direction="both", + surface_streamlines=True, + max_time=0.25, + ) + + pl = pv.Plotter() + + pl.add_arrows( + velocity_points.points, + velocity_points.point_data["V"], + mag=0.01, + opacity=0.75, + ) + + pl.add_points( + passive_swarm_points, + color="Black", + render_points_as_spheres=True, + point_size=5, + opacity=0.5, + ) + + # pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.75) + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=False, + scalars="Omega", + use_transparency=False, + opacity=0.5, + ) + + pl.add_mesh( + pvmesh, + cmap="RdBu", + scalars="Omega", + opacity=0.1, # clim=[0.0, 20.0] + ) + + pl.add_mesh(pvstream, opacity=0.33) + + scale_bar_items = list(pl.scalar_bars.keys()) + + for scalar in scale_bar_items: + pl.remove_scalar_bar(scalar) + + pl.screenshot( + filename="{}.png".format(filename), + window_size=(2560, 2560), + return_img=False, + ) + + # pl.show() + + +ts = 0 + +# + +# Time evolution model / update in time + +for step in range(0, 100): # 250 + delta_t = 2.0 * navier_stokes.estimate_dt() + navier_stokes.solve(timestep=delta_t, zero_init_guess=False) + + passive_swarm.advection(v_soln.sym, delta_t, order=2, corrector=False, evalf=False) + + nodal_vorticity_from_v.solve() + + npoints = 100 + passive_swarm.dm.addNPoints(npoints) + with passive_swarm.access(passive_swarm.particle_coordinates): + for i in range(npoints): + passive_swarm.particle_coordinates.data[-1 : -(npoints + 1) : -1, :] = np.array( + [-0.05, 0.9] + 0.1 * np.random.random((npoints, 2)) + ) + + + if uw.mpi.rank == 0: + print("Timestep {}, dt {}".format(ts, delta_t)) + + if ts % 5 == 0: + plot_V_mesh(filename="output/{}_step_{}".format(expt_name, ts)) + + meshball.write_timestep( + expt_name, + meshUpdates=True, + meshVars=[p_soln, v_soln, vorticity, St], + outputPath="output", + index=ts, + ) + + passive_swarm.write_timestep( + expt_name, + "passive_swarm", + swarmVars=None, + outputPath="output", + index=ts, + force_sequential=True, + ) + + ts += 1 +# - +pvmesh.point_data["V"].min() + +# # ! open . + + + + diff --git a/main/_sources/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Benchmarks_NS_DFG_2d.py b/main/_sources/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Benchmarks_NS_DFG_2d.py new file mode 100644 index 0000000..0cdd38a --- /dev/null +++ b/main/_sources/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Benchmarks_NS_DFG_2d.py @@ -0,0 +1,649 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Navier Stokes test: flow around a circular inclusion (2D) +# +# http://www.mathematik.tu-dortmund.de/~featflow/en/benchmarks/cfdbenchmarking/flow/dfg_benchmark1_re20.html +# +# No slip conditions +# +# ![](http://www.mathematik.tu-dortmund.de/~featflow/media/dfg_bench1_2d/geometry.png) +# +# Note ... +# +# In this benchmark, I have scaled $\rho = 1000$ and $\nu = 1.0$ as otherwise it fails to converge. This occurs because we are locked into a range of $\Delta t$ by the flow velocity (and accurate particle transport), and by the assumption that $\dot{\epsilon}$ is computed in the Eulerian form. The Crank-Nicholson scheme still has some timestep requirements associated with diffusivity (viscosity in this case) and this may be what I am seeing. +# +# Velocity is the same, but pressure scales by 1000. This should encourage us to implement scaling / units. +# +# Model 4 is not one of the benchmarks, but just turns up the Re parameter to see if the mesh can resolve higher values than 100 +# +# + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import os +import petsc4py +import underworld3 as uw +import numpy as np +import sympy + +import psutil + +pid = os.getpid() +python_process = psutil.Process(pid) +print(f"Memory usage = {python_process.memory_info().rss//1000000} Mb", flush=True) + +# + +# Parameters that define the notebook +# These can be set when launching the script as +# mpirun python3 scriptname -uw_resolution=0.1 etc + +resolution = uw.options.getReal("model_resolution", default=16) +refinement = uw.options.getInt("model_refinement", default=0) +model = uw.options.getInt("model_number", default=3) +maxsteps = uw.options.getInt("max_steps", default=1000) +restart_step = uw.options.getInt("restart_step", default=-1) + + +# + +if model == 1: + U0 = 0.3 + expt_name = f"NS_benchmark_DFG2d_1_{resolution}" + +elif model == 2: + U0 = 0.3 + expt_name = f"NS_benchmark_DFG2d_1_ss_{resolution}" + +elif model == 3: + U0 = 1.5 + expt_name = f"NS_benchmark_DFG2d_2iii_{resolution}" + +elif model == 4: + U0 = 3.75 + expt_name = f"NS_test_Re_250_{resolution}" + +elif model == 5: + U0 = 15 + expt_name = f"NS_test_Re_1000_{resolution}" +# - + +outdir = f"output_swarm_{resolution}" +os.makedirs(".meshes", exist_ok=True) +os.makedirs(f"{outdir}", exist_ok=True) + +# + +import pygmsh + +# Mesh a 2D pipe with a circular hole + +csize = 1.0 / resolution +csize_circle = 0.5 * csize +res = csize_circle + +width = 2.2 +height = 0.41 +radius = 0.05 + +from enum import Enum + +## NOTE: stop using pygmsh, then we can just define boundary labels ourselves and not second guess pygmsh + +class boundaries(Enum): + bottom = 1 + right = 2 + top = 3 + left = 4 + inclusion = 5 + All_Boundaries = 1001 + +def pipemesh_mesh_refinement_callback(dm): + r_p = radius + + # print(f"Refinement callback - spherical", flush=True) + + c2 = dm.getCoordinatesLocal() + coords = c2.array.reshape(-1, 2) - centre + + R = np.sqrt(coords[:, 0] ** 2 + coords[:, 1] ** 2).reshape(-1, 1) + + pipeIndices = uw.cython.petsc_discretisation.petsc_dm_find_labeled_points_local( + dm, "inclusion" + ) + + coords[pipeIndices] *= r_p / R[pipeIndices] + coords = coords + centre + + c2.array[...] = coords.reshape(-1) + dm.setCoordinatesLocal(c2) + + return + +## Restore inflow samples to inflow points +def pipemesh_return_coords_to_bounds(coords): + lefty_troublemakers = coords[:, 0] < 0.0 + far_right = coords[:, 0] > 2.2 + too_low = coords[:, 1] < 0.0 + too_high = coords[:, 1] > 0.41 + + coords[lefty_troublemakers, 0] = 0.0001 + coords[far_right, 0] = 2.2 - 0.0001 + + coords[too_low, 1] = 0.0001 + coords[too_high, 1] = 0.41 - 0.0001 + + return coords + +if uw.mpi.rank == 0: + # Generate local mesh on boss process + + with pygmsh.geo.Geometry() as geom: + geom.characteristic_length_max = csize + + inclusion = geom.add_circle( + (0.2, 0.2, 0.0), radius, make_surface=False, mesh_size=csize_circle + ) + domain = geom.add_rectangle( + xmin=0.0, + ymin=0.0, + xmax=width, + ymax=height, + z=0, + holes=[inclusion], + mesh_size=csize, + ) + + geom.add_physical(domain.surface.curve_loop.curves[0], label="bottom") + geom.add_physical(domain.surface.curve_loop.curves[1], label="right") + geom.add_physical(domain.surface.curve_loop.curves[2], label="top") + geom.add_physical(domain.surface.curve_loop.curves[3], label="left") + geom.add_physical(inclusion.curve_loop.curves, label="inclusion") + geom.add_physical(domain.surface, label="Elements") + + geom.generate_mesh(dim=2, verbose=False) + geom.save_geometry(f".meshes/ns_pipe_flow_{resolution}.msh") + +pipemesh = uw.discretisation.Mesh( + f".meshes/ns_pipe_flow_{resolution}.msh", + markVertices=True, + useMultipleTags=True, + useRegions=True, + refinement=refinement, + refinement_callback=pipemesh_mesh_refinement_callback, + return_coords_to_bounds= pipemesh_return_coords_to_bounds, + boundaries=boundaries, + qdegree=3, +) + +pipemesh.dm.view() + + +# radius_fn = sympy.sqrt(pipemesh.rvec.dot(pipemesh.rvec)) # normalise by outer radius if not 1.0 +# unit_rvec = pipemesh.rvec / (1.0e-10+radius_fn) + +# Some useful coordinate stuff + +x = pipemesh.N.x +y = pipemesh.N.y + +# relative to the centre of the inclusion +r = sympy.sqrt((x - 0.2) ** 2 + (y - 0.2) ** 2) +th = sympy.atan2(y - 0.2, x - 0.2) + +# need a unit_r_vec equivalent + +inclusion_rvec = pipemesh.rvec - 1.0 * pipemesh.N.i - 0.5 * pipemesh.N.j +inclusion_unit_rvec = inclusion_rvec / inclusion_rvec.dot(inclusion_rvec) + +# Boundary condition as specified in the diagram + +Vb = (4.0 * U0 * y * (0.41 - y)) / 0.41**2 +# - + + +print(f"Memory usage = {python_process.memory_info().rss//1000000} Mb", flush=True) + +# + +v_soln = uw.discretisation.MeshVariable("U", pipemesh, pipemesh.dim, degree=2) +vs_soln = uw.discretisation.MeshVariable("Us", pipemesh, pipemesh.dim, degree=2) +p_soln = uw.discretisation.MeshVariable("P", pipemesh, 1, degree=1) +vorticity = uw.discretisation.MeshVariable("omega", pipemesh, 1, degree=1) +r_inc = uw.discretisation.MeshVariable("R", pipemesh, 1, degree=1) +rho = uw.discretisation.MeshVariable("rho", pipemesh, 1, degree=1, varsymbol=r"{\rho}") + +# Nodal values of deviatoric stress (symmetric tensor) +work = uw.discretisation.MeshVariable( + "W", pipemesh, 1, vtype=uw.VarType.SCALAR, degree=1, continuous=False +) +St = uw.discretisation.MeshVariable( + r"Stress", + pipemesh, + (2, 2), + vtype=uw.VarType.SYM_TENSOR, + degree=1, + continuous=False, + varsymbol=r"{\tau}", +) + + +# + +swarm = uw.swarm.Swarm(mesh=pipemesh, recycle_rate=6) + +DvDt = uw.systems.ddt.Lagrangian_Swarm( + swarm, + v_soln.sym, + uw.VarType.VECTOR, + degree=2, + order=2, + verbose=False, + continuous=True, +) + + +swarm.populate(fill_param=4) + +# - + + + + +# + +passive_swarm = uw.swarm.Swarm(mesh=pipemesh) +passive_swarm.populate( + fill_param=1, +) + +# add new points at the inflow +npoints = 50 +passive_swarm.dm.addNPoints(npoints) +with passive_swarm.access(passive_swarm.particle_coordinates): + for i in range(npoints): + passive_swarm.particle_coordinates.data[-1 : -(npoints + 1) : -1, :] = np.array( + [0.0, 0.195] + 0.01 * np.random.random((npoints, 2)) + ) + +# - +nodal_vorticity_from_v = uw.systems.Projection(pipemesh, vorticity) +nodal_vorticity_from_v.uw_function = sympy.vector.curl(v_soln.fn).dot(pipemesh.N.k) +nodal_vorticity_from_v.smoothing = 1.0e-3 +nodal_vorticity_from_v.petsc_options.delValue("ksp_monitor") + +# + +# Set solve options here (or remove default values +# stokes.petsc_options.getAll() + +navier_stokes = uw.systems.NavierStokes( + pipemesh, + velocityField=v_soln, + pressureField=p_soln, + DuDt=DvDt, + rho=1000.0, + verbose=False, + solver_name="navier_stokes", + order=2, +) + +navier_stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel + +# Constant visc + +navier_stokes.rho = 1000.0 +navier_stokes.penalty = 0.1 +navier_stokes.bodyforce = sympy.Matrix([0, 0]) + +hw = 1000.0 / res +with pipemesh.access(r_inc): + r_inc.data[:, 0] = uw.function.evalf(r, pipemesh.data, pipemesh.N) + +surface_defn = sympy.exp(-(((r_inc.fn - radius) / radius) ** 2) * hw) + +# Velocity boundary conditions + +navier_stokes.add_dirichlet_bc( + (0.0, 0.0), + "inclusion", +) +navier_stokes.add_dirichlet_bc((0.0, 0.0), "top") +navier_stokes.add_dirichlet_bc((0.0, 0.0), "bottom") +navier_stokes.add_dirichlet_bc((Vb, 0.0), "left") + +navier_stokes.tolerance = 1.0e-3 +navier_stokes.delta_t = 10.0 # stokes-like at the beginning + + +if model == 2: # Steady state ! + # remove the d/dt term ... replace the time dependence with the + # steady state advective transport term + # to lean towards steady state solutions + + navier_stokes.UF0 = -( + navier_stokes.rho * (v_soln.sym - v_soln_1.sym) / navier_stokes.delta_t + ) + +# - +navier_stokes.Unknowns.DuDt.update_pre_solve(0.0) + +# + + +navier_stokes.solve(timestep=10.0, verbose=False) # Stokes-like initial flow +nodal_vorticity_from_v.solve() + + +# + +# stress_projection = uw.systems.Tensor_Projection( +# pipemesh, tensor_Field=St, scalar_Field=work +# ) +# stress_projection.uw_function = navier_stokes.stress_deviator +# stress_projection.solve() + +# with swarm.access(stress_star_p), pipemesh.access(): +# stress_star_p.data[:, 0] = uw.function.evaluate( +# St.sym_1d[0], swarm.particle_coordinates.data +# ) +# stress_star_p.data[:, 1] = uw.function.evaluate( +# St.sym_1d[1], swarm.particle_coordinates.data +# ) +# stress_star_p.data[:, 2] = uw.function.evaluate( +# St.sym_1d[2], swarm.particle_coordinates.data +# ) + + +# + +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(pipemesh) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) + pvmesh.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh, v_soln.sym.dot(v_soln.sym)) + pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym) + + # point sources at cell centres + + points = np.zeros((pipemesh._centroids.shape[0], 3)) + points[:, 0] = pipemesh._centroids[:, 0] + points[:, 1] = pipemesh._centroids[:, 1] + point_cloud = pv.PolyData(points) + + passive_swarm_points = uw.visualisation.swarm_to_pv_cloud(passive_swarm) + active_swarm_points = uw.visualisation.swarm_to_pv_cloud(swarm) + + pvstream = pvmesh.streamlines_from_source( + point_cloud, vectors="V", integration_direction="forward", max_steps=10 + ) + + pl = pv.Plotter(window_size=(1000, 750)) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.025 / U0, opacity=0.75) + + pl.add_points( + active_swarm_points, + color="Black", + render_points_as_spheres=False, + point_size=3, + opacity=0.66, + ) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="Vmag", + use_transparency=False, + opacity=1.0, + ) + + # pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.75) + # pl.add_mesh(pvstream) + + # pl.remove_scalar_bar("mag") + + pl.show() +# + + +if uw.mpi.size == 1: + pl = pv.Plotter(window_size=(1000, 750)) + + +def plot_V_mesh(filename): + + if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(pipemesh) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) + pvmesh.point_data["Omega"] = vis.scalar_fn_to_pv_points(pvmesh, vorticity.sym) + pvmesh.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh, v_soln.sym.dot(v_soln.sym)) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym) + + + # point sources at cell centres for streamlines + + points = np.zeros((pipemesh._centroids.shape[0], 3)) + points[:, 0] = pipemesh._centroids[:, 0] + points[:, 1] = pipemesh._centroids[:, 1] + point_cloud = pv.PolyData(points) + + passive_swarm_points = uw.visualisation.swarm_to_pv_cloud(passive_swarm) + active_swarm_points = uw.visualisation.swarm_to_pv_cloud(swarm) + + pvstream = pvmesh.streamlines_from_source( + point_cloud, vectors="V", integration_direction="forward", max_steps=10, + ) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="Omega", + use_transparency=False, + opacity=1.0, + show_scalar_bar=False, + ) + + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], + mag=0.025 / U0, opacity=0.75, + show_scalar_bar=False) + + pl.add_mesh(pvstream, show_scalar_bar=False) + + pl.add_points( + passive_swarm_points, + color="Black", + render_points_as_spheres=True, + point_size=5, + opacity=0.5, + ) + + + pl.add_points( + active_swarm_points, + color="DarkGreen", + render_points_as_spheres=True, + point_size=2, + opacity=0.25, + ) + + + + pl.camera.SetPosition(0.75, 0.2, 1.5) + pl.camera.SetFocalPoint(0.75, 0.2, 0.0) + pl.camera.SetClippingRange(1.0, 8.0) + + + # pl.camera_position = "xz" + pl.screenshot( + filename="{}.png".format(filename), + window_size=(2560, 1280), + return_img=False, + ) + + pl.clear() + +# - + + + +# + +ts = 0 +elapsed_time = 0.0 +dt_ns = 0.01 +delta_t_cfl = 5 * navier_stokes.estimate_dt() +delta_t = min(delta_t_cfl, dt_ns) + +navier_stokes.DuDt.update(delta_t) +# - + + +for step in range(0, 251): #1500 + delta_t_cfl = 5 * navier_stokes.estimate_dt() + + if step % 10 == 0: + delta_t = min(delta_t_cfl, dt_ns) + + navier_stokes.solve(timestep=dt_ns, zero_init_guess=False) + + # update passive swarm + passive_swarm.advection(v_soln.sym, delta_t, order=2, corrector=False, evalf=False) + + # update material point swarm + swarm.advection(v_soln.sym, delta_t, order=2, corrector=False, evalf=False) + + + # add new points at the inflow + npoints = 200 + passive_swarm.dm.addNPoints(npoints) + with passive_swarm.access(passive_swarm.particle_coordinates): + for i in range(npoints): + passive_swarm.particle_coordinates.data[ + -1 : -(npoints + 1) : -1, : + ] = np.array([0.0, 0.195] + 0.01 * np.random.random((npoints, 2))) + + if uw.mpi.rank == 0: + print("Timestep {}, t {}, dt {}, dt_s {}".format(ts, elapsed_time, delta_t, delta_t_cfl)) + + if ts % 10 == 0: + nodal_vorticity_from_v.solve() + plot_V_mesh(filename=f"{outdir}/{expt_name}.{ts:05d}") + + pipemesh.write_timestep( + expt_name, + meshUpdates=True, + meshVars=[p_soln, v_soln, vorticity, St], + outputPath=outdir, + index=ts, + ) + + passive_swarm.write_timestep( + expt_name, + "passive_swarm", + swarmVars=None, + outputPath=outdir, + index=ts, + force_sequential=True, + ) + + elapsed_time += delta_t + ts += 1 + +# + +# check the mesh if in a notebook / serial + +if 1 and uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(pipemesh) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) + pvmesh.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh, v_soln.sym.dot(v_soln.sym)) + pvmesh.point_data["Omega"] = vis.scalar_fn_to_pv_points(pvmesh, vorticity.sym) + pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym) + + ustar = navier_stokes.Unknowns.DuDt.psi_star[0].sym + pvmesh.point_data["Vs"] = vis.scalar_fn_to_pv_points(pvmesh, ustar.dot(ustar)) + + # point sources at cell centres + points = np.zeros((pipemesh._centroids.shape[0], 3)) + points[:, 0] = pipemesh._centroids[:, 0] + points[:, 1] = pipemesh._centroids[:, 1] + point_cloud = pv.PolyData(points) + + passive_swarm_points = uw.visualisation.swarm_to_pv_cloud(passive_swarm) + active_swarm_points = uw.visualisation.swarm_to_pv_cloud(swarm) + + + pvstream = pvmesh.streamlines_from_source( + point_cloud, vectors="V", integration_direction="forward", max_steps=10 + ) + + points = vis.swarm_to_pv_cloud(passive_swarm) + point_cloud = pv.PolyData(points) + + pl0 = pv.Plotter(window_size=(1000, 750)) + + + pl0.add_points( + passive_swarm_points, + color="DarkGreen", + render_points_as_spheres=False, + point_size=3, + opacity=0.66, + ) + + pl0.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="Omega", + use_transparency=False, + opacity=1.0, + ) + + pl0.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.03 / U0, opacity=0.75) + pl0.add_mesh(pvstream) + + pl0.add_points( + active_swarm_points, + color="Black", + render_points_as_spheres=True, + point_size=3, + opacity=0.5, + ) + + pl0.show() +# - + + + + diff --git a/main/_sources/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Benchmarks_NS_DFG_2d_SLCN.py b/main/_sources/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Benchmarks_NS_DFG_2d_SLCN.py new file mode 100644 index 0000000..0ba6b12 --- /dev/null +++ b/main/_sources/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Benchmarks_NS_DFG_2d_SLCN.py @@ -0,0 +1,566 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Navier Stokes test: flow around a circular inclusion (2D) +# +# http://www.mathematik.tu-dortmund.de/~featflow/en/benchmarks/cfdbenchmarking/flow/dfg_benchmark1_re20.html +# +# No slip conditions +# +# ![](http://www.mathematik.tu-dortmund.de/~featflow/media/dfg_bench1_2d/geometry.png) +# +# Note ... +# +# In this benchmark, I have scaled $\rho = 1000$ and $\nu = 1.0$ as otherwise it fails to converge. This occurs because we are locked into a range of $\Delta t$ by the flow velocity (and accurate particle transport), and by the assumption that $\dot{\epsilon}$ is computed in the Eulerian form. The Crank-Nicholson scheme still has some timestep requirements associated with diffusivity (viscosity in this case) and this may be what I am seeing. +# +# Velocity is the same, but pressure scales by 1000. This should encourage us to implement scaling / units for this notebook. +# +# Model 4 is not one of the benchmarks, but just turns up the Re parameter to see if the mesh can resolve higher values than 100 +# +# + +# + +import os + +os.environ["UW_TIMING_ENABLE"] = "1" + +import petsc4py +import underworld3 as uw +from underworld3 import timing + +import nest_asyncio +nest_asyncio.apply() + +import numpy as np +import sympy + +# import psutil +# pid = os.getpid() +# python_process = psutil.Process(pid) +# print(f"Memory usage = {python_process.memory_info().rss//1000000} Mb", flush=True) + +# + +# Parameters that define the notebook +# These can be set when launching the script as +# mpirun python3 scriptname -uw_resolution=0.1 etc + +resolution = uw.options.getInt("model_resolution", default=30) +refinement = uw.options.getInt("model_refinement", default=0) +model = uw.options.getInt("model_number", default=4) +maxsteps = uw.options.getInt("max_steps", default=1000) +restart_step = uw.options.getInt("restart_step", default=-1) + +# + +if model == 1: + U0 = 0.3 + expt_name = f"NS_benchmark_DFG2d_SLCN_1_{resolution}" + +elif model == 2: + U0 = 0.3 + expt_name = f"NS_benchmark_DFG2d_SLCN_1_ss_{resolution}" + +elif model == 3: + U0 = 1.5 + expt_name = f"NS_benchmark_DFG2d_SLCN_2_{resolution}" + +elif model == 4: + U0 = 3.75 + expt_name = f"NS_test_Re_250_SLCN_{resolution}" + +elif model == 5: + U0 = 15 + expt_name = f"NS_test_Re_1000i_SLCN_{resolution}" +# - + +outdir = f"output/output_res_{resolution}" +os.makedirs(".meshes", exist_ok=True) +os.makedirs(f"{outdir}", exist_ok=True) + +# + +import pygmsh +from enum import Enum + +## NOTE: stop using pygmsh, then we can just define boundary labels ourselves and not second guess pygmsh + +class boundaries(Enum): + bottom = 1 + right = 2 + top = 3 + left = 4 + inclusion = 5 + All_Boundaries = 1001 + +# Mesh a 2D pipe with a circular hole + +csize = 1.0 / resolution +csize_circle = 0.5 * csize +res = csize_circle + +width = 2.2 +height = 0.41 +radius = 0.05 +centre = (0.2, 0.2) + +def pipemesh_mesh_refinement_callback(dm): + r_p = radius + + # print(f"Refinement callback - spherical", flush=True) + + c2 = dm.getCoordinatesLocal() + coords = c2.array.reshape(-1, 2) - centre + + R = np.sqrt(coords[:, 0] ** 2 + coords[:, 1] ** 2).reshape(-1, 1) + + pipeIndices = uw.cython.petsc_discretisation.petsc_dm_find_labeled_points_local( + dm, "inclusion" + ) + + coords[pipeIndices] *= r_p / R[pipeIndices] + coords = coords + centre + + c2.array[...] = coords.reshape(-1) + dm.setCoordinatesLocal(c2) + + return + +## Restore inflow samples to inflow points +def pipemesh_return_coords_to_bounds(coords): + lefty_troublemakers = coords[:, 0] < 0.0 + coords[lefty_troublemakers, 0] = 0.0001 + + return coords + + + +if uw.mpi.rank == 0: + # Generate local mesh on boss process + + with pygmsh.geo.Geometry() as geom: + geom.characteristic_length_max = csize + + inclusion = geom.add_circle( + (centre[0], centre[1], 0.0), + radius, + make_surface=False, + mesh_size=csize_circle, + ) + domain = geom.add_rectangle( + xmin=0.0, + ymin=0.0, + xmax=width, + ymax=height, + z=0, + holes=[inclusion], + mesh_size=csize, + ) + + geom.add_physical(domain.surface.curve_loop.curves[0], label=boundaries.bottom.name) + geom.add_physical(domain.surface.curve_loop.curves[1], label=boundaries.right.name) + geom.add_physical(domain.surface.curve_loop.curves[2], label=boundaries.top.name) + geom.add_physical(domain.surface.curve_loop.curves[3], label=boundaries.left.name) + geom.add_physical(inclusion.curve_loop.curves, label=boundaries.inclusion.name) + geom.add_physical(domain.surface, label="Elements") + + geom.generate_mesh(dim=2, verbose=False) + geom.save_geometry(f".meshes/ns_pipe_flow_{resolution}.msh") + +pipemesh = uw.discretisation.Mesh( + f".meshes/ns_pipe_flow_{resolution}.msh", + markVertices=True, + useMultipleTags=True, + useRegions=True, + refinement=refinement, + refinement_callback=pipemesh_mesh_refinement_callback, + return_coords_to_bounds= pipemesh_return_coords_to_bounds, + boundaries=boundaries, + qdegree=3, +) + +pipemesh.dm.view() + + +# Some useful coordinate stuff + +x = pipemesh.N.x +y = pipemesh.N.y + +# relative to the centre of the inclusion +r = sympy.sqrt((x - 0.2) ** 2 + (y - 0.2) ** 2) +th = sympy.atan2(y - 0.2, x - 0.2) + +# need a unit_r_vec equivalent + +inclusion_rvec = pipemesh.rvec - 1.0 * pipemesh.N.i - 0.5 * pipemesh.N.j +inclusion_unit_rvec = inclusion_rvec / inclusion_rvec.dot(inclusion_rvec) + +# Boundary condition as specified in the diagram + +Vb = (4.0 * U0 * y * (0.41 - y)) / 0.41**2 + + +# + +v_soln = uw.discretisation.MeshVariable("U", pipemesh, pipemesh.dim, degree=2) +vs_soln = uw.discretisation.MeshVariable("Us", pipemesh, pipemesh.dim, degree=2) +p_soln = uw.discretisation.MeshVariable("P", pipemesh, 1, degree=1) +vorticity = uw.discretisation.MeshVariable("omega", pipemesh, 1, degree=1) +r_inc = uw.discretisation.MeshVariable("R", pipemesh, 1, degree=1) +rho = uw.discretisation.MeshVariable("rho", pipemesh, 1, degree=1, varsymbol=r"{\rho}") + +# Nodal values of deviatoric stress (symmetric tensor) +work = uw.discretisation.MeshVariable( + "W", pipemesh, 1, vtype=uw.VarType.SCALAR, degree=1, continuous=False +) +St = uw.discretisation.MeshVariable( + r"Stress", + pipemesh, + (2, 2), + vtype=uw.VarType.SYM_TENSOR, + degree=1, + continuous=False, + varsymbol=r"{\tau}", +) + + +# + +passive_swarm = uw.swarm.Swarm(mesh=pipemesh) +passive_swarm.populate( + fill_param=1, +) + +# add new points at the inflow +npoints = 100 +passive_swarm.dm.addNPoints(npoints) +with passive_swarm.access(passive_swarm.particle_coordinates): + for i in range(npoints): + passive_swarm.particle_coordinates.data[-1 : -(npoints + 1) : -1, :] = np.array( + [0.01, 0.195] + 0.01 * np.random.random((npoints, 2)) + ) + +# - +nodal_vorticity_from_v = uw.systems.Projection(pipemesh, vorticity) +nodal_vorticity_from_v.uw_function = sympy.vector.curl(v_soln.fn).dot(pipemesh.N.k) +nodal_vorticity_from_v.smoothing = 1.0e-3 +nodal_vorticity_from_v.petsc_options.delValue("ksp_monitor") + +# + +# Set solve options here (or remove default values +# stokes.petsc_options.getAll() + +navier_stokes = uw.systems.NavierStokesSLCN( + pipemesh, + velocityField=v_soln, + pressureField=p_soln, + rho=1000.0, + verbose=False, + solver_name="navier_stokes", + order=2, +) + +navier_stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel + +# Constant visc + +navier_stokes.penalty = 0 +navier_stokes.bodyforce = sympy.Matrix([0, 0]) + +hw = 1000.0 / res +with pipemesh.access(r_inc): + r_inc.data[:, 0] = uw.function.evalf(r, pipemesh.data, pipemesh.N) + +surface_defn = sympy.exp(-(((r_inc.fn - radius) / radius) ** 2) * hw) + +# Velocity boundary conditions + +navier_stokes.add_dirichlet_bc( + (0.0, 0.0), + "inclusion", +) +navier_stokes.add_dirichlet_bc((0.0, 0.0), "top") +navier_stokes.add_dirichlet_bc((0.0, 0.0), "bottom") +navier_stokes.add_dirichlet_bc((Vb, 0.0), "left") + +navier_stokes.tolerance = 1.0e-3 +navier_stokes.delta_t = 10.0 # stokes-like at the beginning + +if model == 2: # Steady state ! + # remove the d/dt term ... replace the time dependence with the + # steady state advective transport term + # to lean towards steady state solutions + + navier_stokes.UF0 = -( + navier_stokes.rho * (v_soln.sym - v_soln_1.sym) / navier_stokes.delta_t + ) + +# - + + +navier_stokes.view() + +navier_stokes.constitutive_model.flux + +# + +navier_stokes.petsc_options["snes_monitor"] = None +navier_stokes.petsc_options["ksp_monitor"] = None + +navier_stokes.petsc_options["snes_type"] = "newtonls" +navier_stokes.petsc_options["ksp_type"] = "fgmres" + +navier_stokes.petsc_options.setValue("fieldsplit_velocity_pc_type", "mg") +navier_stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_type", "kaskade") +navier_stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_cycle_type", "w") + +navier_stokes.petsc_options["fieldsplit_velocity_mg_coarse_pc_type"] = "svd" +navier_stokes.petsc_options["fieldsplit_velocity_ksp_type"] = "fcg" +navier_stokes.petsc_options["fieldsplit_velocity_mg_levels_ksp_type"] = "chebyshev" +navier_stokes.petsc_options["fieldsplit_velocity_mg_levels_ksp_max_it"] = 2 +navier_stokes.petsc_options["fieldsplit_velocity_mg_levels_ksp_converged_maxits"] = None + +# # gasm is super-fast ... but mg seems to be bulletproof +# # gamg is toughest wrt viscosity + +# navier_stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "gamg") +# navier_stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "additive") +# navier_stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v") + +# # # mg, multiplicative - very robust ... similar to gamg, additive + +navier_stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "mg") +navier_stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "multiplicative") +navier_stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v") + +# + +timing.reset() +timing.start() + +navier_stokes.solve( + timestep=10, verbose=False, +) # Stokes-like initial flow + +nodal_vorticity_from_v.solve() + +timing.print_table(display_fraction=0.999) + + +# - +def plot_V_mesh(filename): + + if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(pipemesh) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) + pvmesh.point_data["Omega"] = vis.scalar_fn_to_pv_points(pvmesh, vorticity.sym) + pvmesh.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh, v_soln.sym.dot(v_soln.sym)) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym) + + pl = pv.Plotter(window_size=(1000, 750)) + + # point sources at cell centres for streamlines + + points = np.zeros((pipemesh._centroids.shape[0], 3)) + points[:, 0] = pipemesh._centroids[:, 0] + points[:, 1] = pipemesh._centroids[:, 1] + point_cloud = pv.PolyData(points) + + passive_swarm_points = uw.visualisation.swarm_to_pv_cloud(passive_swarm) + + pvstream = pvmesh.streamlines_from_source( + point_cloud, vectors="V", integration_direction="forward", max_steps=10, + ) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="Omega", + use_transparency=False, + opacity=1.0, + show_scalar_bar=False, + ) + + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], + mag=0.025 / U0, opacity=0.75, + show_scalar_bar=False) + + pl.add_mesh(pvstream, show_scalar_bar=False) + + pl.add_points( + passive_swarm_points, + color="Black", + render_points_as_spheres=True, + point_size=5, + opacity=0.5, + ) + + + pl.camera.SetPosition(0.75, 0.2, 1.5) + pl.camera.SetFocalPoint(0.75, 0.2, 0.0) + pl.camera.SetClippingRange(1.0, 8.0) + + + # pl.camera_position = "xz" + pl.screenshot( + filename="{}.png".format(filename), + window_size=(2560, 1280), + return_img=False, + ) + + pl.clear() + + + +ts = 0 +elapsed_time = 0.0 +dt_ns = 0.01 +delta_t_cfl = 2 * navier_stokes.estimate_dt() +delta_t = min(delta_t_cfl, dt_ns) + + +for step in range(0, maxsteps): #1500 + delta_t_cfl = 2 * navier_stokes.estimate_dt() + + if step % 10 == 0: + delta_t = min(delta_t_cfl, dt_ns) + + navier_stokes.solve(timestep=dt_ns, zero_init_guess=False) + + # update passive swarm + passive_swarm.advection(v_soln.sym, delta_t, order=2, corrector=False, evalf=False) + + # add new points at the inflow + npoints = 200 + passive_swarm.dm.addNPoints(npoints) + with passive_swarm.access(passive_swarm.particle_coordinates): + for i in range(npoints): + passive_swarm.particle_coordinates.data[ + -1 : -(npoints + 1) : -1, : + ] = np.array([0.0, 0.195] + 0.01 * np.random.random((npoints, 2))) + + if uw.mpi.rank == 0: + print("Timestep {}, t {}, dt {}, dt_s {}".format(ts, elapsed_time, delta_t, delta_t_cfl)) + + if ts % 10 == 0: + nodal_vorticity_from_v.solve() + plot_V_mesh(filename=f"{outdir}/{expt_name}.{ts:05d}") + + pipemesh.write_timestep( + expt_name, + meshUpdates=True, + meshVars=[p_soln, v_soln, vorticity, St], + outputPath=outdir, + index=ts, + ) + + passive_swarm.write_timestep( + expt_name, + "passive_swarm", + swarmVars=None, + outputPath=outdir, + index=ts, + force_sequential=True, + ) + + elapsed_time += delta_t + ts += 1 +# + +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(pipemesh) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) + pvmesh.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh, v_soln.sym.dot(v_soln.sym)) + pvmesh.point_data["Omega"] = vis.scalar_fn_to_pv_points(pvmesh, vorticity.sym) + pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym) + + ustar = navier_stokes.Unknowns.DuDt.psi_star[0].sym + pvmesh.point_data["Vs"] = vis.scalar_fn_to_pv_points(pvmesh, ustar.dot(ustar)) + + # point sources at cell centres + points = np.zeros((pipemesh._centroids.shape[0], 3)) + points[:, 0] = pipemesh._centroids[:, 0] + points[:, 1] = pipemesh._centroids[:, 1] + point_cloud = pv.PolyData(points) + + passive_swarm_points = uw.visualisation.swarm_to_pv_cloud(passive_swarm) + + + pvstream = pvmesh.streamlines_from_source( + point_cloud, vectors="V", integration_direction="forward", max_steps=10 + ) + + points = vis.swarm_to_pv_cloud(passive_swarm) + point_cloud = pv.PolyData(points) + + pl = pv.Plotter(window_size=(1000, 750)) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.025 / U0, opacity=0.75) + + pl.add_points( + point_cloud, + color="Black", + render_points_as_spheres=False, + point_size=5, + opacity=0.66, + ) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="Omega", + use_transparency=False, + opacity=1.0, + ) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.025 / U0, opacity=0.75) + pl.add_mesh(pvstream) + + pl.add_points( + passive_swarm_points, + color="Black", + render_points_as_spheres=True, + point_size=5, + opacity=0.5, + ) + + pl.show() + + +# + +# with pipemesh.access(): +# print(navier_stokes.DFDt.psi_star[0].data.max()) + +# + +# navier_stokes._u_f1 +# - + + + + + + diff --git a/main/_sources/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Disk_Coriolis.py b/main/_sources/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Disk_Coriolis.py new file mode 100644 index 0000000..e25874e --- /dev/null +++ b/main/_sources/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Disk_Coriolis.py @@ -0,0 +1,311 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + + +# # Cylindrical Stokes with Coriolis term (out of plane) + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import petsc4py +from petsc4py import PETSc + +import underworld3 as uw +import numpy as np +import sympy + + +# + +model = 1 + +if model == 1: + free_slip = False + expt_name = "NS_flow_coriolis_disk_no_slip" + +elif model == 2: + free_slip = True + expt_name = "NS_flow_coriolis_disk_free_slip" + +# - + + +meshball = uw.meshing.Annulus( + radiusOuter=1.0, + radiusInner=0.0, + cellSize=0.05, + qdegree=3, + filename="tmp_CoriolisDisk.msh", +) + +# + +v_soln = uw.discretisation.MeshVariable("U", meshball, 2, degree=2) +p_soln = uw.discretisation.MeshVariable("P", meshball, 1, degree=1) +t_soln = uw.discretisation.MeshVariable("T", meshball, 1, degree=3) + +v_soln_1 = uw.discretisation.MeshVariable("U_1", meshball, meshball.dim, degree=2) +vorticity = uw.discretisation.MeshVariable("omega", meshball, 1, degree=1) + + +# + +swarm = uw.swarm.Swarm(mesh=meshball) +v_star = uw.swarm.SwarmVariable("Vs", swarm, meshball.dim, proxy_degree=3) +remeshed = uw.swarm.SwarmVariable("Vw", swarm, 1, proxy_degree=3, dtype="int") +X_0 = uw.swarm.SwarmVariable("X0", swarm, meshball.dim, _proxy=False) + +swarm.populate(fill_param=4) + +# + +# Create a density structure / buoyancy force +# gravity will vary linearly from zero at the centre +# of the sphere to (say) 1 at the surface + +import sympy + +radius_fn = sympy.sqrt( + meshball.rvec.dot(meshball.rvec) +) # normalise by outer radius if not 1.0 + +unit_rvec = meshball.CoordinateSystem.unit_e_0 +gravity_fn = radius_fn + +# Some useful coordinate stuff + +x = meshball.N.x +y = meshball.N.y + +r = sympy.sqrt(x**2 + y**2) +th = sympy.atan2(y + 1.0e-5, x + 1.0e-5) + +# + +Rayleigh = 1.0e2 +# + +# Surface-drive flow, use this bc + +# vtheta = r * sympy.sin(th) +# vx = -vtheta*sympy.sin(th) +# vy = vtheta*sympy.cos(th) + +# + +# Create NS object + +navier_stokes = uw.systems.NavierStokesSwarm( + meshball, + velocityField=v_soln, + pressureField=p_soln, + velocityStar_fn=v_star.sym, + rho=1.0, + theta=0.5, + verbose=False, + projection=False, + solver_name="navier_stokes", +) + +navier_stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel(meshball.dim) +navier_stokes.constitutive_model.Parameters.viscosity = 1 +navier_stokes.rho = 1000.0 +navier_stokes.theta = 0.5 +navier_stokes.bodyforce = sympy.Matrix([0, 0]) + +navier_stokes.saddle_preconditioner = ( + 1.0 / navier_stokes.constitutive_model.Parameters.viscosity +) + +hw = 1000.0 / meshball.get_min_radius() +surface_fn = sympy.exp(-((r - 1.0) ** 2) * hw) +free_slip_penalty = ( + 1.0e4 * Rayleigh * v_soln.sym.dot(unit_rvec) * unit_rvec * surface_fn +) + +# Velocity boundary conditions + +if free_slip: + free_slip_penalty = ( + 1.0e4 * Rayleigh * v_soln.sym.dot(unit_rvec) * unit_rvec * surface_fn + ) +else: + free_slip_penalty = 0.0 + navier_stokes.add_dirichlet_bc((0.0, 0.0), "Upper", (0, 1)) + +navier_stokes.add_dirichlet_bc((0.0, 0.0), "Centre", (0, 1)) + +v_theta = ( + navier_stokes.theta * navier_stokes.u.sym + + (1.0 - navier_stokes.theta) * navier_stokes.u_star_fn +) +# - + +t_init = sympy.cos(3 * th) + +# + +# Write density into a variable for saving + +with meshball.access(t_soln): + t_soln.data[:, 0] = uw.function.evaluate(t_init, t_soln.coords, meshball.N) + +# - +navier_stokes.bodyforce = Rayleigh * unit_rvec * t_init # minus * minus +navier_stokes.bodyforce -= free_slip_penalty + + +# + +navier_stokes.solve(timestep=10.0) + +with meshball.access(): + v_inertial = v_soln.data.copy() + +with swarm.access(v_star, remeshed, X_0): + v_star.data[...] = uw.function.evaluate(v_soln.fn, swarm.data) + X_0.data[...] = swarm.data[...] + +# - + +swarm.advection(v_soln.fn, delta_t=navier_stokes.estimate_dt(), corrector=False) + + +# + +# check the mesh if in a notebook / serial + +def plot_V_mesh(filename): + if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(navier_stokes.u) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, navier_stokes.u.sym) + + pl = pv.Plotter(window_size=(1000, 750)) + pl.camera.SetPosition(0.0001, 0.0001, 4.0) + + # pl.add_mesh(pvmesh,'Black', 'wireframe') + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + use_transparency=False, + opacity=0.5, + ) + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.2) + + pl.screenshot( + filename="{}.png".format(filename), + window_size=(2560, 2560), + return_img=False, + ) + + +# + +ts = 0 +swarm_loop = 10 + +Omega = 2.0 * meshball.N.k +navier_stokes.bodyforce = Rayleigh * unit_rvec * t_init # minus * minus +navier_stokes.bodyforce -= free_slip_penalty +navier_stokes.bodyforce -= ( + 2.0 * navier_stokes.rho * meshball.vector.cross(Omega, v_theta) +) +# - + + +for step in range(0, 250): + delta_t = 5.0 * navier_stokes.estimate_dt() + + navier_stokes.solve(timestep=delta_t, zero_init_guess=False) + + dv_fn = v_soln.fn - v_soln_1.fn + _, _, _, _, _, _, deltaV = meshball.stats(dv_fn.dot(dv_fn)) + + with meshball.access(v_soln_1): + v_soln_1.data[...] = 0.5 * v_soln_1.data[...] + 0.5 * v_soln.data[...] + + with swarm.access(v_star): + v_star.data[...] = uw.function.evaluate(v_soln.fn, swarm.data) + + swarm.advection(v_soln.fn, delta_t=delta_t, corrector=False) + + # Restore a subset of points to start + offset_idx = step % swarm_loop + + with swarm.access(swarm.particle_coordinates, remeshed): + remeshed.data[...] = 0 + remeshed.data[offset_idx::swarm_loop, :] = 1 + swarm.data[offset_idx::swarm_loop, :] = X_0.data[offset_idx::swarm_loop, :] + + # re-calculate v history for remeshed particles + # Note, they may have moved procs after the access manager closed + # so we re-index + + with swarm.access(v_star, remeshed): + idx = np.where(remeshed.data == 1)[0] + v_star.data[idx] = uw.function.evaluate(v_soln.fn, swarm.data[idx]) + + if uw.mpi.rank == 0: + print("Timestep {}, dt {}, deltaV {}".format(ts, delta_t, deltaV)) + + if ts % 1 == 0: + # nodal_vorticity_from_v.solve() + plot_V_mesh(filename="output/{}_step_{}".format(expt_name, ts)) + + # savefile = "output/{}_ts_{}.h5".format(expt_name,step) + # meshball.save(savefile) + # v_soln.save(savefile) + # p_soln.save(savefile) + # vorticity.save(savefile) + # meshball.generate_xdmf(savefile) + + ts += 1 + + +# check the mesh if in a notebook / serial +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(navier_stokes.u) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, navier_stokes.u.sym) + + pl = pv.Plotter(window_size=(1000, 750)) + + # pl.add_mesh(pvmesh,'Black', 'wireframe') + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + use_transparency=False, + opacity=0.5, + ) + pl.add_arrows(arrow_loc, arrow_length, mag=0.2) + + pl.show(cpos="xy") + + +pl.camera.GetClippingRange() + +((v_inertial - usol) ** 2).mean() + +v_inertial.max() + +# # diff --git a/main/_sources/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Disk_FreeSlipBCs-Coriolis.py b/main/_sources/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Disk_FreeSlipBCs-Coriolis.py new file mode 100644 index 0000000..747d08e --- /dev/null +++ b/main/_sources/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Disk_FreeSlipBCs-Coriolis.py @@ -0,0 +1,362 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Cylindrical Stokes with Coriolis term (out of plane) + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import petsc4py +from petsc4py import PETSc + +import underworld3 as uw +import numpy as np +import sympy + +# - + + +expt_name = "NS_FS_flow_coriolis_disk_500_iii" + +# + +import meshio + +# meshball = uw.meshes.SphericalShell( +# dim=2, radius_outer=1.0, radius_inner=0.0, cell_size=0.075, degree=1, verbose=False +# ) + +meshball = uw.meshing.Annulus(radiusOuter=1.0, radiusInner=0.0, cellSize=0.05, degree=1, centre=False, verbosity=True) + +# + +v_soln = uw.discretisation.MeshVariable("U", meshball, 2, degree=2) +p_soln = uw.discretisation.MeshVariable("P", meshball, 1, degree=1) +t_soln = uw.discretisation.MeshVariable("T", meshball, 1, degree=3) +r = uw.discretisation.MeshVariable("R", meshball, 1, degree=1) + + +v_soln_1 = uw.discretisation.MeshVariable("U_1", meshball, meshball.dim, degree=2) +vorticity = uw.discretisation.MeshVariable("omega", meshball, 1, degree=1) + + +# + +swarm = uw.swarm.Swarm(mesh=meshball) +v_star = uw.swarm.SwarmVariable("Vs", swarm, meshball.dim, proxy_degree=3) +remeshed = uw.swarm.SwarmVariable("Vw", swarm, 1, dtype="int", _proxy=False) +X_0 = uw.swarm.SwarmVariable("X0", swarm, meshball.dim, _proxy=False) + +swarm.populate(fill_param=4) + +# + +# Create a density structure / buoyancy force +# gravity will vary linearly from zero at the centre +# of the sphere to (say) 1 at the surface + +import sympy + +radius_fn = sympy.sqrt( + meshball.rvec.dot(meshball.rvec) +) # normalise by outer radius if not 1.0 +unit_rvec = meshball.rvec / (1.0e-10 + radius_fn) +gravity_fn = radius_fn + +# Some useful coordinate stuff + +x = meshball.N.x +y = meshball.N.y + +# r = sympy.sqrt(x**2+y**2) +th = sympy.atan2(y + 1.0e-5, x + 1.0e-5) + +# +Rayleigh = 1.0e2 + +# +hw = 1000.0 / 0.075 +surface_fn = sympy.exp(-(((r.fn - 1.0) / 1.0) ** 2) * hw) +# - +orientation_wrt_z = sympy.atan2(y + 1.0e-10, x + 1.0e-10) +v_rbm_z_x = -r.fn * sympy.sin(orientation_wrt_z) * meshball.N.i +v_rbm_z_y = r.fn * sympy.cos(orientation_wrt_z) * meshball.N.j +v_rbm_z = v_rbm_z_x + v_rbm_z_y + +# + +# Create NS object + +navier_stokes = uw.systems.NavierStokesSwarm( + meshball, + velocityField=v_soln, + pressureField=p_soln, + velocityStar_fn=v_star.fn, + u_degree=v_soln.degree, + p_degree=p_soln.degree, + rho=1.0, + theta=0.5, + verbose=False, + projection=True, + solver_name="navier_stokes", +) + +navier_stokes.petsc_options.delValue( + "ksp_monitor" +) # We can flip the default behaviour at some point +navier_stokes._u_star_projector.petsc_options.delValue("ksp_monitor") +navier_stokes._u_star_projector.petsc_options["snes_rtol"] = 1.0e-2 +navier_stokes._u_star_projector.petsc_options["snes_type"] = "newtontr" +navier_stokes._u_star_projector.smoothing = 0.0 # navier_stokes.viscosity * 1.0e-6 +navier_stokes._u_star_projector.penalty = 0.0 + +# Constant visc + +navier_stokes.rho = 1.0 +navier_stokes.theta = 0.5 +navier_stokes.penalty = 0.0 +navier_stokes.viscosity = 1.0 + +# Free slip condition by penalizing radial velocity at the surface (non-linear term) +free_slip_penalty = 1.0e4 * Rayleigh * v_soln.fn.dot(unit_rvec) * unit_rvec * surface_fn + +# Velocity boundary conditions + +# navier_stokes.add_dirichlet_bc( (0.0, 0.0), "Upper", (0,1)) +# navier_stokes.add_dirichlet_bc( (0.0, 0.0), "Centre", (0,1)) + +v_theta = ( + navier_stokes.theta * navier_stokes.u.fn + + (1.0 - navier_stokes.theta) * navier_stokes.u_star_fn +) + +# - + +nodal_vorticity_from_v = uw.systems.Projection(meshball, vorticity) +nodal_vorticity_from_v.uw_function = sympy.vector.curl(v_soln.fn).dot(meshball.N.k) +nodal_vorticity_from_v.smoothing = 1.0e-3 + +t_init = sympy.cos(3 * th) + +# + +with meshball.access(r): + r.data[:, 0] = uw.function.evaluate( + sympy.sqrt(x**2 + y**2), meshball.data + ) # cf radius_fn which is 0->1 + +# Write density into a variable for saving + +with meshball.access(t_soln): + t_soln.data[:, 0] = uw.function.evaluate(t_init, t_soln.coords) + +# + +navier_stokes.bodyforce = Rayleigh * unit_rvec * t_init # minus * minus +navier_stokes.bodyforce -= free_slip_penalty # + solid_body_penalty + +v_proj = navier_stokes._u_star_projector.u +free_slip_penalty_p = 100 * v_proj.fn.dot(unit_rvec) * unit_rvec * surface_fn +navier_stokes._u_star_projector.F0 = free_slip_penalty_p # + solid_body_penalty_p) + + +# + +navier_stokes.solve(timestep=10.0) +nodal_vorticity_from_v.solve() + +with meshball.access(): + v_inertial = v_soln.data.copy() + +with swarm.access(v_star, remeshed, X_0): + v_star.data[...] = uw.function.evaluate(v_soln.fn, swarm.data) + X_0.data[...] = swarm.data[...] + +# - + +swarm.advection(v_soln.fn, delta_t=navier_stokes.estimate_dt(), corrector=False) + + +# + +# check the mesh if in a notebook / serial + + +def plot_V_mesh(filename): + + if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym) + pvmesh.point_data["Om"] = vis.scalar_fn_to_pv_points(pvmesh, vorticity.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(navier_stokes.u) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, navier_stokes.u.sym) + + pl = pv.Plotter(window_size=(1000, 750)) + pl.camera.SetPosition(0.0001, 0.0001, 4.0) + + # pl.add_mesh(pvmesh,'Black', 'wireframe') + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="Om", + use_transparency=False, + opacity=0.5, + ) + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.03) + + pl.screenshot( + filename="{}.png".format(filename), + window_size=(2560, 2560), + return_img=False, + ) + + pl.close() + + del pl + + +# - + + +ts = 0 +swarm_loop = 5 + +vorticity.fn + +for step in range(0, 10): + + Omega_0 = 50.0 * min(ts / 10, 1.0) + Coriolis = ( + 2.0 * Omega_0 * navier_stokes.rho * sympy.vector.cross(meshball.N.k, v_theta) + ) + + navier_stokes.bodyforce = Rayleigh * unit_rvec * t_init # minus * minus + navier_stokes.bodyforce -= free_slip_penalty + navier_stokes.bodyforce -= Coriolis * (1.0 - surface_fn) + + delta_t = 1.0 * navier_stokes.estimate_dt() + + navier_stokes.solve(timestep=delta_t, zero_init_guess=False) + nodal_vorticity_from_v.solve() + + _, z_ns, _, _, _, _, _ = meshball.stats(v_soln.fn.dot(v_rbm_z)) + print("Rigid body: {}".format(z_ns)) + + dv_fn = v_soln.fn - v_soln_1.fn + _, _, _, _, _, _, deltaV = meshball.stats(dv_fn.dot(dv_fn)) + + with meshball.access(v_soln_1): + v_soln_1.data[...] = v_soln.data[...] + + with swarm.access(v_star): + v_star.data[...] = ( + 0.5 * uw.function.evaluate(v_soln.fn, swarm.data) + 0.5 * v_star.data[...] + ) + + swarm.advection(v_soln.fn, delta_t=delta_t, corrector=False) + + # Restore a subset of points to start + offset_idx = step % swarm_loop + + with swarm.access(swarm.particle_coordinates, remeshed): + remeshed.data[...] = 0 + remeshed.data[offset_idx::swarm_loop, :] = 1 + swarm.data[offset_idx::swarm_loop, :] = X_0.data[offset_idx::swarm_loop, :] + + # re-calculate v history for remeshed particles + # Note, they may have moved procs after the access manager closed + # so we re-index + + with swarm.access(v_star, remeshed): + idx = np.where(remeshed.data == 1)[0] + v_star.data[idx] = uw.function.evaluate(v_soln.fn, swarm.data[idx]) + + if uw.mpi.rank == 0: + print("Timestep {}, dt {}, deltaV {}".format(ts, delta_t, deltaV)) + + if ts % 1 == 0: + # nodal_vorticity_from_v.solve() + plot_V_mesh(filename="output/{}_step_{}".format(expt_name, ts)) + + # savefile = "output/{}_ts_{}.h5".format(expt_name,step) + # meshball.save(savefile) + # v_soln.save(savefile) + # p_soln.save(savefile) + # vorticity.save(savefile) + # meshball.generate_xdmf(savefile) + + navier_stokes._u_star_projector.smoothing = navier_stokes.viscosity * 1.0e-6 + + ts += 1 + + +navier_stokes._p_f0 + + +# + +# check the mesh if in a notebook / serial + + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym) + pvmesh.point_data["Om"] = vis.scalar_fn_to_pv_points(pvmesh, vorticity.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(navier_stokes.u) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, navier_stokes.u.sym) + + pl = pv.Plotter(window_size=[1000, 1000]) + + # pl.add_mesh(pvmesh,'Black', 'wireframe') + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="Om", + use_transparency=False, + opacity=0.5, + ) + pl.add_arrows(arrow_loc, arrow_length, mag=0.05) + + pl.show(cpos="xy") +# - + + +meshball.stats( + sympy.vector.cross(Omega, v_soln.fn).dot(sympy.vector.cross(Omega, v_soln.fn)) +) + +meshball.stats(v_soln.fn.dot(v_rbm_z)) + +meshball.stats(v_soln.fn.dot(v_soln.fn)) + +meshball.stats(v_soln.fn.dot(v_rbm_z)) + +meshball.stats((v_soln.fn + 0.015 * v_rbm_z).dot(v_rbm_z)) + +meshball.stats(sympy.vector.cross(Omega, v_soln.fn).dot(v_rbm_z)) + +sympy.vector.cross(Omega, v_soln.fn) + +_, z_ns, _, _, _, _, _ = meshball.stats(v_soln.fn.dot(v_rbm_z)) +print("Rigid body: {}".format(z_ns)) + +x_ns_ diff --git a/main/_sources/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Disk_NoSlipBCs-Coriolis_ss.py b/main/_sources/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Disk_NoSlipBCs-Coriolis_ss.py new file mode 100644 index 0000000..2a61c09 --- /dev/null +++ b/main/_sources/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Disk_NoSlipBCs-Coriolis_ss.py @@ -0,0 +1,338 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Cylindrical Stokes with Coriolis term (out of plane) +# +# We implement Stokes/Navier-Stokes flow in a disc with rigid bc's and take into account the effect of Coriolis terms +# $2\Omega \times \mathbf{u}$. +# +# The non-linear implementation in Stokes flow relies on the Newton (SNES) solver but the structure of the solution (with strong Coriolis terms) +# is very different from the Stokes flow pattern (especially the pressure gradients) and so we instead set up a Navier-Stokes problem in which +# we can down-weight the inertial effects but retain a time-evolution terms to approach the solution. In this example, I am using the same +# trick as for the SS benchmark case where I try to suppress the time dependent term in the Navier-Stokes equation and replace timesteps with +# pseudo timesteps. +# +# +# This works best if we spin-up the rotation gradually. +# + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import petsc4py +from petsc4py import PETSc + +import underworld3 as uw +import numpy as np +import sympy + +# - + + +expt_name = "SS_NS_flow_coriolis_10" + +# + +import meshio + +# meshball = uw.meshes.SphericalShell( +# dim=2, radius_outer=1.0, radius_inner=0.0, cell_size=0.05, degree=1, verbose=True +# ) + +meshball = uw.meshing.Annulus(radiusOuter=1.0, radiusInner=0.0, cellSize=0.05, degree=1, centre=False, verbosity=True) + +# + +v_soln = uw.discretisation.MeshVariable("U", meshball, 2, degree=2) +p_soln = uw.discretisation.MeshVariable("P", meshball, 1, degree=1) +t_soln = uw.discretisation.MeshVariable("T", meshball, 1, degree=3) + +v_soln_1 = uw.discretisation.MeshVariable("U_1", meshball, meshball.dim, degree=2) +vorticity = uw.discretisation.MeshVariable("omega", meshball, 1, degree=1) + + +# + +swarm = uw.swarm.Swarm(mesh=meshball) +v_star = uw.swarm.SwarmVariable("Vs", swarm, meshball.dim, proxy_degree=3) +remeshed = uw.swarm.SwarmVariable("Vw", swarm, 1, proxy_degree=3, dtype="int") +X_0 = uw.swarm.SwarmVariable("X0", swarm, meshball.dim, _proxy=False) + +swarm.populate(fill_param=4) + +# + +# Create a density structure / buoyancy force +# gravity will vary linearly from zero at the centre +# of the sphere to (say) 1 at the surface + +import sympy + +radius_fn = sympy.sqrt( + meshball.rvec.dot(meshball.rvec) +) # normalise by outer radius if not 1.0 +unit_rvec = meshball.rvec / (1.0e-10 + radius_fn) +gravity_fn = radius_fn + +# Some useful coordinate stuff + +x = meshball.N.x +y = meshball.N.y + +r = sympy.sqrt(x**2 + y**2) +th = sympy.atan2(y + 1.0e-5, x + 1.0e-5) + +# + +Rayleigh = 1.0e2 +# + +# Surface-drive flow, use this bc + +# vtheta = r * sympy.sin(th) +# vx = -vtheta*sympy.sin(th) +# vy = vtheta*sympy.cos(th) + +# + +# Create NS object + +navier_stokes = uw.systems.NavierStokesSwarm( + meshball, + velocityField=v_soln, + pressureField=p_soln, + velocityStar_fn=v_star.fn, + u_degree=v_soln.degree, + p_degree=p_soln.degree, + rho=1.0, + theta=0.5, + verbose=False, + projection=True, + solver_name="navier_stokes", +) + +navier_stokes.petsc_options.delValue( + "ksp_monitor" +) # We can flip the default behaviour at some point +navier_stokes._u_star_projector.petsc_options.delValue("ksp_monitor") +navier_stokes._u_star_projector.petsc_options["snes_rtol"] = 1.0e-2 +navier_stokes._u_star_projector.petsc_options["snes_type"] = "newtontr" +navier_stokes._u_star_projector.smoothing = 0.0 # navier_stokes.viscosity * 1.0e-6 +navier_stokes._u_star_projector.penalty = 0.0001 + +# Here we replace the time dependence with the steady state advective transport term +# to lean towards steady state solutions + +navier_stokes.UF0 = ( + -navier_stokes.rho * (v_soln.fn - v_soln_1.fn) / navier_stokes.delta_t +) + +# Constant visc + +navier_stokes.rho = 1000.0 +navier_stokes.theta = 0.5 +navier_stokes.penalty = 0.0 +navier_stokes.viscosity = 1.0 +navier_stokes.bodyforce = 1.0e-32 * meshball.N.i +navier_stokes._Ppre_fn = 1.0 / ( + navier_stokes.viscosity + navier_stokes.rho / navier_stokes.delta_t +) + +# Velocity boundary conditions + +navier_stokes.add_dirichlet_bc((0.0, 0.0), "Upper", (0, 1)) +navier_stokes.add_dirichlet_bc((0.0, 0.0), "Centre", (0, 1)) + +v_theta = ( + navier_stokes.theta * navier_stokes.u.fn + + (1.0 - navier_stokes.theta) * navier_stokes.u_star_fn +) +# - + +t_init = sympy.cos(3 * th) + +# + +# Write density into a variable for saving + +with meshball.access(t_soln): + t_soln.data[:, 0] = uw.function.evaluate(t_init, t_soln.coords) + print(t_soln.data.min(), t_soln.data.max()) +# - +navier_stokes.bodyforce = Rayleigh * unit_rvec * t_init # minus * minus + +# + +navier_stokes.solve(timestep=10.0) + +with meshball.access(): + v_inertial = v_soln.data.copy() + +with swarm.access(v_star, remeshed, X_0): + v_star.data[...] = uw.function.evaluate(v_soln.fn, swarm.data) + X_0.data[...] = swarm.data[...] + +# - + +swarm.advection(v_soln.fn, delta_t=navier_stokes.estimate_dt(), corrector=False) + + +# + +# check the mesh if in a notebook / serial + + +def plot_V_mesh(filename): + + if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(navier_stokes.u) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, navier_stokes.u.sym) + + pl = pv.Plotter(window_size=(1000, 750)) + pl.camera.SetPosition(0.0001, 0.0001, 4.0) + + # pl.add_mesh(pvmesh,'Black', 'wireframe') + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + use_transparency=False, + opacity=0.5, + ) + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.05) + + pl.screenshot( + filename="{}.png".format(filename), + window_size=(2560, 2560), + return_img=False, + ) + + pl.close() + + del pl + + +# - + + +ts = 0 +swarm_loop = 5 + + +# + + +for step in range(0, 50): + + Omega = 10.0 * meshball.N.k * min(ts / 25, 1.0) + navier_stokes.bodyforce = Rayleigh * unit_rvec * t_init # minus * minus + navier_stokes.bodyforce -= ( + 2.0 * navier_stokes.rho * sympy.vector.cross(Omega, v_theta) + ) + + delta_t = 10.0 * navier_stokes.estimate_dt() + + navier_stokes.solve(timestep=delta_t, zero_init_guess=False) + + dv_fn = v_soln.fn - v_soln_1.fn + _, _, _, _, _, _, deltaV = meshball.stats(dv_fn.dot(dv_fn)) + + with meshball.access(v_soln_1): + v_soln_1.data[...] = 0.5 * v_soln_1.data[...] + 0.5 * v_soln.data[...] + + with swarm.access(v_star): + v_star.data[...] = uw.function.evaluate(v_soln.fn, swarm.data) + + swarm.advection(v_soln.fn, delta_t=delta_t, corrector=False) + + # Restore a subset of points to start + offset_idx = step % swarm_loop + + with swarm.access(swarm.particle_coordinates, remeshed): + remeshed.data[...] = 0 + remeshed.data[offset_idx::swarm_loop, :] = 1 + swarm.data[offset_idx::swarm_loop, :] = X_0.data[offset_idx::swarm_loop, :] + + # re-calculate v history for remeshed particles + # Note, they may have moved procs after the access manager closed + # so we re-index + + with swarm.access(v_star, remeshed): + idx = np.where(remeshed.data == 1)[0] + v_star.data[idx] = uw.function.evaluate(v_soln.fn, swarm.data[idx]) + + if uw.mpi.rank == 0: + print( + "Iteration (pseudo timestep) {}, dt {}, deltaV {}".format( + ts, delta_t, deltaV + ) + ) + + if ts % 1 == 0: + # nodal_vorticity_from_v.solve() + plot_V_mesh(filename="output/{}_step_{}".format(expt_name, ts)) + + # savefile = "output/{}_ts_{}.h5".format(expt_name,step) + # meshball.save(savefile) + # v_soln.save(savefile) + # p_soln.save(savefile) + # vorticity.save(savefile) + # meshball.generate_xdmf(savefile) + + navier_stokes._u_star_projector.smoothing = navier_stokes.viscosity * 1.0e-6 + + ts += 1 + + +# + +# check the mesh if in a notebook / serial + + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(navier_stokes.u) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, navier_stokes.u.sym) + + pl = pv.Plotter(window_size=(1000, 750)) + + # pl.add_mesh(pvmesh,'Black', 'wireframe') + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="P", + use_transparency=False, + opacity=0.5, + ) + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.033) + + pl.show(cpos="xy") +# - + + +((v_inertial - usol) ** 2).mean() + +v_inertial.max() + +# + +# # diff --git a/main/_sources/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Visualise_NS_DFG_2d.py b/main/_sources/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Visualise_NS_DFG_2d.py new file mode 100644 index 0000000..983bbe6 --- /dev/null +++ b/main/_sources/Notebooks/Examples-NavierStokes/Ex_Navier_Stokes_Visualise_NS_DFG_2d.py @@ -0,0 +1,137 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + + +# # Navier Stokes test: flow around a circular inclusion (2D) +# +# http://www.mathematik.tu-dortmund.de/~featflow/en/benchmarks/cfdbenchmarking/flow/dfg_benchmark1_re20.html +# +# No slip conditions +# +# ![](http://www.mathematik.tu-dortmund.de/~featflow/media/dfg_bench1_2d/geometry.png) +# +# Note ... +# +# In this benchmark, I have scaled $\rho = 1000$ and $\nu = 1.0$ as otherwise it fails to converge. This occurs because we are locked into a range of $\Delta t$ by the flow velocity (and accurate particle transport), and by the assumption that $\dot{\epsilon}$ is computed in the Eulerian form. The Crank-Nicholson scheme still has some timestep requirements associated with diffusivity (viscosity in this case) and this may be what I am seeing. +# +# Velocity is the same, but pressure scales by 1000. This should encourage us to implement scaling / units. +# +# Model 4 is not one of the benchmarks, but just turns up the Re parameter to see if the mesh can resolve higher values than 100 +# +# + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +import os +import petsc4py +import underworld3 as uw +import numpy as np +import sympy + +# ls -trl ~/+Simulations/NS_benchmarks/Re100_dt0.01_proj0_tau2_pr2_rbf | tail + +# + +## Reading the checkpoints back in ... + +step = 230 +basename = f"/Users/lmoresi/+Simulations/NS_benchmarks/Re250_res001/NS_test_Re_250_0.01" +mesh_filename = f"{basename}.mesh.50.h5" +mesh_filename + +# + +mesh = uw.discretisation.Mesh(mesh_filename) + +v_soln_ckpt = uw.discretisation.MeshVariable("U", mesh, mesh.dim, degree=2) +p_soln_ckpt = uw.discretisation.MeshVariable("P", mesh, 1, degree=1) +vorticity_ckpt = uw.discretisation.MeshVariable("omega", mesh, 1, degree=1) + +passive_swarm_ckpt = uw.swarm.Swarm(mesh, recycle_rate=0) + + +# + +v_soln_ckpt.read_from_vertex_checkpoint(f"{basename}.U.{step}.h5", "U") +p_soln_ckpt.read_from_vertex_checkpoint(f"{basename}.P.{step}.h5", "P") +vorticity_ckpt.read_from_vertex_checkpoint(f"{basename}.omega.{step}.h5", "omega") + +# passive_swarm_ckpt.load(f"output/NS_test_Re_250_{res}.passive_swarm.{step}.h5") + +# + +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln_ckpt.sym) + pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln_ckpt.sym) + pvmesh.point_data["Omega"] = vis.scalar_fn_to_pv_points(pvmesh, vorticity_ckpt.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln_ckpt) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln_ckpt.sym) + + # swarm points + +# with passive_swarm_ckpt.access(): +# points = np.zeros((passive_swarm_ckpt.data.shape[0], 3)) +# points[:, 0] = passive_swarm_ckpt.data[:, 0] +# points[:, 1] = passive_swarm_ckpt.data[:, 1] + +# swarm_point_cloud = pv.PolyData(points) + + # point sources at cell centres + + skip = 10 + points = np.zeros((mesh._centroids[::skip].shape[0], 3)) + points[:, 0] = mesh._centroids[::skip, 0] + points[:, 1] = mesh._centroids[::skip, 1] + point_cloud = pv.PolyData(points) + + pvstream = pvmesh.streamlines_from_source( + point_cloud, + vectors="V", + integration_direction="forward", + max_time=0.5, + ) + + pl = pv.Plotter(window_size=(1000, 750)) + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.001, opacity=0.75) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=False, + scalars="Omega", + use_transparency=False, + opacity=1.0, + ) + + # pl.add_points(swarm_point_cloud, color="Black", + # render_points_as_spheres=True, + # point_size=5, opacity=0.66 + # ) + + pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.75) + pl.add_mesh(pvstream, opacity=0.33) + + # pl.remove_scalar_bar("S") + # pl.remove_scalar_bar("mag") + + pl.show() +# - + diff --git a/main/_sources/Notebooks/Examples-NavierStokes/Readme.md b/main/_sources/Notebooks/Examples-NavierStokes/Readme.md new file mode 100644 index 0000000..0d9d4b4 --- /dev/null +++ b/main/_sources/Notebooks/Examples-NavierStokes/Readme.md @@ -0,0 +1,15 @@ +# Navier Stokes Equation + +## Recent solver and visualization updates + +- [x] Ex_Navier_Stokes_Benchmarks_NS_DFG_2d_SLCN.py +- [ ] Ex_Navier_Stokes_Benchmarks_NS_DFG_2d.py + - [ ] require updates near uw.swarm.Lagrangian_Updater +- [ ] Ex_Navier_Stokes_Disk_Coriolis.py + - [ ] require updates near uw.systems.NavierStokesSwarm +- [ ] Ex_Navier_Stokes_Disk_FreeSlipBCs-Coriolis.py + - [ ] require updates near uw.systems.NavierStokesSwarm +- [ ] Ex_Navier_Stokes_Disk_NoSlipBCs-Coriolis_ss.py + - [ ] require updates near uw.systems.NavierStokesSwarm +- [x] Ex_Navier_Stokes_Visualise_NS_DFG_2d.py +- [x] Ex_NavierStokesRotationTest.py diff --git a/main/_sources/Notebooks/Examples-NavierStokes/output/README.md b/main/_sources/Notebooks/Examples-NavierStokes/output/README.md new file mode 100644 index 0000000..d1e0dce --- /dev/null +++ b/main/_sources/Notebooks/Examples-NavierStokes/output/README.md @@ -0,0 +1,3 @@ +# Navier-Stokes model outputs + +These files are NOT under version control \ No newline at end of file diff --git a/main/_sources/Notebooks/Examples-PoissonEquation/Ex_DiffusionSLCN_Cartesian-NL.py b/main/_sources/Notebooks/Examples-PoissonEquation/Ex_DiffusionSLCN_Cartesian-NL.py new file mode 100644 index 0000000..102da4e --- /dev/null +++ b/main/_sources/Notebooks/Examples-PoissonEquation/Ex_DiffusionSLCN_Cartesian-NL.py @@ -0,0 +1,310 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Nonlinear diffusion of a hot pipe +# +# - Using the adv_diff solver. +# - No advection as the velocity field is not updated (and set to 0). +# - Comparison between 1D numerical solution and 2D UW model. +# + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +from petsc4py import PETSc +import underworld3 as uw +from underworld3.systems import Stokes +import numpy as np +import sympy +from mpi4py import MPI + +import math + +if uw.mpi.size == 1: + import matplotlib.pyplot as plt + + +# %% +sys = PETSc.Sys() +sys.pushErrorHandler("traceback") + + +# %% +### Set the resolution. +res = 32 + +xmin, xmax = 0.0, 1.0 +ymin, ymax = 0.0, 1.0 + +pipe_thickness = 0.4 ### + +# %% +k0 = 1e-6 ### m2/s (diffusivity) +l0 = 1e5 ### 100 km in m (length of box) +time_scale = l0**2 / k0 ### s +time_scale_Myr = time_scale / (60 * 60 * 24 * 365.25 * 1e6) + +mesh = uw.meshing.UnstructuredSimplexBox( + minCoords=(xmin, ymin), maxCoords=(xmax, ymax), cellSize=1.0 / res, regular=True +) + +# + +# mesh = uw.meshing.StructuredQuadBox( +# elementRes=(int(res), int(res)), minCoords=(xmin, ymin), maxCoords=(xmax, ymax) +# ) +# - + + +# Create adv_diff object + +# Set some things +k = 1.0 + +tmin = 0.5 +tmax = 1.0 + +# Create an adv +v = uw.discretisation.MeshVariable("U", mesh, mesh.dim, degree=2) +T = uw.discretisation.MeshVariable("T", mesh, 1, degree=1) +k = uw.discretisation.MeshVariable("k", mesh, 1, degree=1) + +dTdY = uw.discretisation.MeshVariable( + r"\partial T/ \partial \mathbf{y}", mesh, 1, degree=2 +) + + +adv_diff = uw.systems.AdvDiffusionSLCN( + mesh, + u_Field=T, + V_fn=v, + solver_name="adv_diff", + ) + +adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel + + +# %% +delT = mesh.vector.gradient(T.sym) +gradient = delT.dot(delT) + +k_sym = (delT.dot(delT)) / 2.0 + +adv_diff.constitutive_model.Parameters.diffusivity = k_sym + +# %% +k_model = uw.systems.Projection(mesh, k) +k_model.uw_function = adv_diff.constitutive_model.Parameters.diffusivity +k_model.smoothing = 1.0e-3 +### set diffusivity BCs +# k_model.add_dirichlet_bc(0., ["Top", "Bottom"], components=0) + + +def updateFields(): + k_model.uw_function = adv_diff.constitutive_model.Parameters.diffusivity + k_model.solve(_force_setup=True) + + +### fix temp of top and bottom walls +adv_diff.add_dirichlet_bc(0.5, "Bottom", 0) +adv_diff.add_dirichlet_bc(0.5, "Top", 0) + + +maxY = mesh.data[:, 1].max() +minY = mesh.data[:, 1].min() + +with mesh.access(T): + T.data[...] = tmin + + pipePosition = ((maxY - minY) - pipe_thickness) / 2.0 + + T.data[ + (mesh.data[:, 1] >= (mesh.data[:, 1].min() + pipePosition)) + & (mesh.data[:, 1] <= (mesh.data[:, 1].max() - pipePosition)) + ] = tmax + + +def plot_fig(): + updateFields() + + if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, T.sym) + pvmesh.point_data["k"] = vis.scalar_fn_to_pv_points(pvmesh, k.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(v) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v.sym) + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh(pvmesh, "Black", "wireframe") + + # pvmesh.point_data["rho"] = uw.function.evaluate(density, mesh.data) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="k", + use_transparency=False, + opacity=0.95, + ) + + # pl.add_mesh(pvmesh, cmap="coolwarm", edge_color="Black", show_edges=True, scalars="S", + # use_transparency=False, opacity=0.5) + + # pl.add_mesh( + # point_cloud, + # cmap="coolwarm", + # edge_color="Black", + # show_edges=False, + # scalars="M", + # use_transparency=False, + # opacity=0.95, + # ) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=5.0, opacity=0.5) + # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1) + + # pl.add_points(pdata) + + pl.show(cpos="xy") + + # return vsol + + +plot_fig() + +# ## Vertical profile across the centre of the box + +### y coords to sample +sample_y = np.arange( + mesh.data[:, 1].min(), mesh.data[:, 1].max(), mesh.get_min_radius() +) ### Vertical profile + +### x coords to sample +# sample_x = np.repeat(mesh.data[:,0].min(), sample_y.shape[0]) ### LHS wall +sample_x = np.zeros_like(sample_y) ### centre of the box + +sample_points = np.empty((sample_x.shape[0], 2)) +sample_points[:, 0] = sample_x +sample_points[:, 1] = sample_y + +t0 = uw.function.evaluate(adv_diff.u.fn, sample_points) + + +def get_dt(): + updateFields() + with mesh.access(k): + ### estimate the timestep based on diffusion only + dt = ( + mesh.get_min_radius() ** 2 / k.data[:, 0].max() + ) ### dt = length squared / diffusivity + + # print(f'dt: {dt*time_scale_Myr} Myr') + print(f"dt: {dt*time_scale_Myr}") + + return dt + + +def diffusion_1D(sample_points, tempProfile, k, model_dt): + x = sample_points + T = tempProfile + + dx = sample_points[1] - sample_points[0] + + dt = 0.5 * (dx**2 / k) + + """ max time of model """ + total_time = model_dt + + """ get min of 1D and 2D model """ + time_1DModel = min(model_dt, dt) + + """ determine number of its """ + nts = math.ceil(total_time / time_1DModel) + + """ get dt of 1D model """ + final_dt = total_time / nts + + for i in range(nts): + qT = -k * np.diff(T) / dx + dTdt = -np.diff(qT) / dx + T[1:-1] += dTdt * final_dt + + return T + + +### get the initial temp profile +tempData = uw.function.evaluate(adv_diff.u.fn, sample_points) + +step = 0 +time = 0.0 + +nsteps = 1 # 21 + +adv_diff.petsc_options["ksp_rtol"] = 1.0e-8 + +adv_diff.petsc_options["snes_rtol"] = 1.0e-8 + +# + +# if uw.mpi.size == 1: +# ''' create figure to show the temp diffuses ''' +# plt.figure(figsize=(9, 3)) +# plt.plot(t0, sample_points[:,1], ls=':') +# - + + +while step < nsteps: + ### print some stuff + if uw.mpi.rank == 0: + # print(f"Step: {str(step).rjust(3)}, time: {time*time_scale_Myr:6.2f} [MYr]") + print(f"Step: {str(step).rjust(3)}, time: {time:6.5f}") + + ### 1D profile from underworld + t1 = uw.function.evaluate(adv_diff.u.fn, sample_points) + + if uw.mpi.size == 1 and step % 10 == 0: + """compare 1D and 2D models""" + plt.figure() + ### profile from UW + plt.plot(t1, sample_points[:, 1], ls="-", c="red", label="2D nonlinear model") + ### numerical solution + plt.plot(tempData, sample_points[:, 1], ls=":", c="k", label="1D linear model") + plt.legend() + plt.show() + + dt = get_dt() + + ### 1D diffusion + tempData = diffusion_1D( + sample_points=sample_points[:, 1], tempProfile=tempData, k=1.0, model_dt=dt + ) + + ### diffuse through underworld + adv_diff.solve(timestep=dt) + + step += 1 + time += dt + +plt.show() + +plot_fig() + + diff --git a/main/_sources/Notebooks/Examples-PoissonEquation/Ex_DiffusionSLCN_Cartesian.py b/main/_sources/Notebooks/Examples-PoissonEquation/Ex_DiffusionSLCN_Cartesian.py new file mode 100644 index 0000000..7f268d0 --- /dev/null +++ b/main/_sources/Notebooks/Examples-PoissonEquation/Ex_DiffusionSLCN_Cartesian.py @@ -0,0 +1,266 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Linear diffusion of a hot pipe +# +# - Using the adv_diff solver. +# - No advection as the velocity field is not updated (and set to 0). +# - Benchmark comparison between 1D numerical solution and 2D UW model. +# + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# %% +from petsc4py import PETSc +import underworld3 as uw +from underworld3.systems import Stokes +import numpy as np +import sympy +from mpi4py import MPI + +import math + +if uw.mpi.size == 1: + import matplotlib.pyplot as plt + + +# %% +sys = PETSc.Sys() +sys.pushErrorHandler("traceback") + + +# %% +# Set the resolution. +res = 32 + +xmin, xmax = 0.0, 1.0 +ymin, ymax = 0.0, 1.0 + +pipe_thickness = 0.4 ### + +# %% +k0 = 1e-6 ### m2/s (diffusivity) +l0 = 1e5 ### 100 km in m (length of box) +time_scale = l0**2 / k0 ### s +time_scale_Myr = time_scale / (60 * 60 * 24 * 365.25 * 1e6) + +# mesh = uw.meshing.UnstructuredSimplexBox(minCoords=(xmin, ymin), maxCoords=(xmax, ymax), cellSize=1.0 / res, regular=True) + +mesh = uw.meshing.StructuredQuadBox( + elementRes=(int(res), int(res)), minCoords=(xmin, ymin), maxCoords=(xmax, ymax) +) + + +# Create adv_diff object + +# Set some things +k = 1.0 + +tmin = 0.5 +tmax = 1.0 + +# Create an adv +v = uw.discretisation.MeshVariable("U", mesh, mesh.dim, degree=2) +T = uw.discretisation.MeshVariable("T", mesh, 1, degree=1) + +adv_diff = uw.systems.AdvDiffusionSLCN( + mesh, + u_Field=T, + V_fn=v, + solver_name="adv_diff", +) + +adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel +adv_diff.constitutive_model.Parameters.diffusivity = k + +# %% +### fix temp of top and bottom walls +adv_diff.add_dirichlet_bc(0.5, "Bottom", 0) +adv_diff.add_dirichlet_bc(0.5, "Top", 0) + + +# %% +maxY = mesh.data[:, 1].max() +minY = mesh.data[:, 1].min() + +with mesh.access(T): + T.data[...] = tmin + + pipePosition = ((maxY - minY) - pipe_thickness) / 2.0 + + T.data[ + (mesh.data[:, 1] >= (mesh.data[:, 1].min() + pipePosition)) + & (mesh.data[:, 1] <= (mesh.data[:, 1].max() - pipePosition)) + ] = tmax + + +# %% +def plot_fig(): + if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, T.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(v) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v.sym) + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh(pvmesh, "Black", "wireframe") + + # pvmesh.point_data["rho"] = uw.function.evaluate(density, mesh.data) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="T", + use_transparency=False, + opacity=0.95, + ) + + # pl.add_mesh(pvmesh, cmap="coolwarm", edge_color="Black", show_edges=True, scalars="S", + # use_transparency=False, opacity=0.5) + + # pl.add_mesh( + # point_cloud, + # cmap="coolwarm", + # edge_color="Black", + # show_edges=False, + # scalars="M", + # use_transparency=False, + # opacity=0.95, + # ) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=5.0, opacity=0.5) + # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1) + + # pl.add_points(pdata) + + pl.show(cpos="xy") + + # return vsol + + +plot_fig() + +# ## Vertical profile across the centre of the box + +### y coords to sample +sample_y = np.arange( + mesh.data[:, 1].min(), mesh.data[:, 1].max(), mesh.get_min_radius() +) ### Vertical profile + +### x coords to sample +# sample_x = np.repeat(mesh.data[:,0].min(), sample_y.shape[0]) ### LHS wall +sample_x = np.zeros_like(sample_y) ### centre of the box + +sample_points = np.empty((sample_x.shape[0], 2)) +sample_points[:, 0] = sample_x +sample_points[:, 1] = sample_y + +t0 = uw.function.evaluate(adv_diff.u.fn, sample_points) + +# %% +### estimate the timestep based on diffusion only +dt = mesh.get_min_radius() ** 2 / k ### dt = length squared / diffusivity +# print(f'dt: {dt*time_scale_Myr} Myr') +print(f"dt: {dt*time_scale_Myr}") + + +# %% +def diffusion_1D(sample_points, tempProfile, k, model_dt): + x = sample_points + T = tempProfile + + dx = sample_points[1] - sample_points[0] + + dt = 0.5 * (dx**2 / k) + + """ max time of model """ + total_time = model_dt + + """ get min of 1D and 2D model """ + time_1DModel = min(model_dt, dt) + + """ determine number of its """ + nts = math.ceil(total_time / time_1DModel) + + """ get dt of 1D model """ + final_dt = total_time / nts + + for i in range(nts): + qT = -k * np.diff(T) / dx + dTdt = -np.diff(qT) / dx + T[1:-1] += dTdt * final_dt + + return T + + +### get the initial temp profile +tempData = uw.function.evaluate(adv_diff.u.fn, sample_points) + +# + + +step = 0 +time = 0.0 +# - + +# %% +nsteps = 1 # 21 + +# + + +if uw.mpi.size == 1: + """create figure to show the temp diffuses""" + plt.figure(figsize=(9, 3)) + plt.plot(t0, sample_points[:, 1], ls=":") +# - + + +while step < nsteps: + ### print some stuff + if uw.mpi.rank == 0: + # print(f"Step: {str(step).rjust(3)}, time: {time*time_scale_Myr:6.2f} [MYr]") + print(f"Step: {str(step).rjust(3)}, time: {time:6.5f}") + + ### 1D profile from underworld + t1 = uw.function.evalf(adv_diff.u.sym[0], sample_points) + + if uw.mpi.size == 1 and step % 10 == 0: + """compare 1D and 2D models""" + plt.figure() + ### profile from UW + plt.plot(t1, sample_points[:, 1], ls="-", c="red", label="2D linear model") + ### numerical solution + plt.plot(tempData, sample_points[:, 1], ls=":", c="k", label="1D linear model") + plt.legend() + plt.show() + + ### 1D diffusion + tempData = diffusion_1D( + sample_points=sample_points[:, 1], tempProfile=tempData, k=k, model_dt=dt + ) + + ### diffuse through underworld + adv_diff.solve(timestep=dt) + + step += 1 + time += dt diff --git a/main/_sources/Notebooks/Examples-PoissonEquation/Ex_Poisson_Cartesian.py b/main/_sources/Notebooks/Examples-PoissonEquation/Ex_Poisson_Cartesian.py new file mode 100644 index 0000000..1f8f142 --- /dev/null +++ b/main/_sources/Notebooks/Examples-PoissonEquation/Ex_Poisson_Cartesian.py @@ -0,0 +1,349 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Poisson Equation (simple) +# +# First we show how this works using the generic class and then the minor differences for +# the `Poisson` class +# +# ## Generic scalar solver class + + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +from petsc4py import PETSc + +import os + +os.environ["UW_TIMING_ENABLE"] = "1" + +import underworld3 as uw +from underworld3 import timing + +import numpy as np +import sympy + +from IPython.display import display + + +# + +mesh1 = uw.meshing.UnstructuredSimplexBox( + minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0), cellSize=1.0 / 4, refinement=4 +) + +mesh2 = uw.meshing.UnstructuredSimplexBox( + minCoords=(0.0, 0.0), + maxCoords=(1.0, 1.0), + cellSize=1.0 / 4, + regular=True, + refinement=4, +) +# - + +# pick a mesh +mesh = mesh1 + +phi = uw.discretisation.MeshVariable("Phi", mesh, 1, degree=2, varsymbol=r"\phi") +scalar = uw.discretisation.MeshVariable( + "Theta", mesh, 1, degree=1, continuous=False, varsymbol=r"\Theta" +) + +# Create Poisson object + +poisson = uw.systems.Poisson(mesh, u_Field=phi, solver_name="diffusion") + +# Constitutive law (diffusivity) + +poisson.constitutive_model = uw.constitutive_models.DiffusionModel +poisson.constitutive_model.Parameters.diffusivity = 1 + + +# %% +poisson.constitutive_model.c + +# + +# Set some things +poisson.f = 0.0 +poisson.add_dirichlet_bc(1.0, "Bottom", components=0) +poisson.add_dirichlet_bc(0.0, "Top", components=0) + +poisson.tolerance = 1.0e-6 +poisson.petsc_options["snes_type"] = "newtonls" +poisson.petsc_options["ksp_type"] = "fgmres" + +poisson.petsc_options["snes_monitor"] = None +poisson.petsc_options["ksp_monitor"] = None +poisson.petsc_options.setValue("pc_type", "mg") +poisson.petsc_options.setValue("pc_mg_type", "multiplicative") +poisson.petsc_options.setValue("pc_mg_type", "kaskade") +# poisson.petsc_options["mg_levels"] = mesh.dm.getRefineLevel()-2 +poisson.petsc_options["mg_levels_ksp_type"] = "fgmres" +poisson.petsc_options["mg_levels_ksp_max_it"] = 100 +poisson.petsc_options["mg_levels_ksp_converged_maxits"] = None +poisson.petsc_options["mg_coarse_pc_type"] = "svd" + +# - + +poisson.view() + +poisson._setup_pointwise_functions(verbose=True) + +poisson._setup_discretisation() + +timing.reset() +timing.start() + +# %% +# Solve time +poisson.solve() + +type(poisson.F1) + +# %% +# Check. Construct simple linear function which is solution for +# above config. Exclude boundaries from mesh data. +import numpy as np + +with mesh.access(): + mesh_numerical_soln = uw.function.evalf(poisson.u.fn, mesh.data) + mesh_analytic_soln = uw.function.evalf(1.0 - mesh.N.y, mesh.data) + if not np.allclose(mesh_analytic_soln, mesh_numerical_soln, rtol=0.0001): + print("Unexpected values encountered.") + + +# Validate + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh) + + pvmesh.point_data["T"] = mesh_analytic_soln + pvmesh.point_data["T2"] = mesh_numerical_soln + pvmesh.point_data["DT"] = pvmesh.point_data["T"] - pvmesh.point_data["T2"] + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="DT", + use_transparency=False, + opacity=0.5, + # scalar_bar_args=sargs, + ) + + pl.camera_position = "xy" + + pl.show(cpos="xy") + # pl.screenshot(filename="test.png") + +# Create some arbitrary function using one of the base scalars x,y[,z] = mesh.X + +import sympy + +x, y = mesh.X +x0 = y0 = 1 / sympy.sympify(2) +k = sympy.exp(-((x - x0) ** 2 + (y - y0) ** 2)) + +poisson.constitutive_model.Parameters.diffusivity = k + +poisson.constitutive_model.flux + +with mesh.access(): + orig_soln = poisson.u.data.copy() + +orig_soln_mesh = uw.function.evalf(phi.sym[0], mesh.data) + +# %% +poisson.solve(zero_init_guess=True, _force_setup=True) + +print(poisson.Unknowns.u.stats()) + +# Simply confirm results are different + +with mesh.access(): + if np.allclose(poisson.u.data, orig_soln, rtol=0.001): + raise RuntimeError("Values did not change !") + + +mesh._evaluation_hash = None + +# Visual validation + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh2 = vis.mesh_to_pv_mesh(mesh) + + pvmesh2.point_data["T"] = uw.function.evaluate(phi.sym[0], mesh.data) + pvmesh2.point_data["Te"] = uw.function.evalf(phi.sym[0], mesh.data) + pvmesh2.point_data["dT"] = pvmesh2.point_data["T"] - pvmesh.point_data["T"] + pvmesh2.point_data["dTe"] = pvmesh2.point_data["T"] - pvmesh2.point_data["Te"] + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh( + pvmesh2, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="dT", + use_transparency=False, + opacity=0.5, + # scalar_bar_args=sargs, + ) + + pl.camera_position = "xy" + + pl.show(cpos="xy") + # pl.screenshot(filename="test.png") + + +# ## Non-linear example + + +# RHS term + +abs_r2 = x**2 + y**2 +poisson.f = -16 * abs_r2 +poisson.add_dirichlet_bc(abs_r2, "Bottom", components=0) +poisson.add_dirichlet_bc(abs_r2, "Top", components=0) +poisson.add_dirichlet_bc(abs_r2, "Right", components=0) +poisson.add_dirichlet_bc(abs_r2, "Left", components=0) + +display(poisson.f) + +# Constitutive law (diffusivity) +# Linear solver first + +poisson.constitutive_model = uw.constitutive_models.DiffusionModel +poisson.constitutive_model.Parameters.diffusivity = 1 + +poisson.solve() + + +# Non-linear diffusivity + +grad_phi = mesh.vector.gradient(phi.sym) +k = 5 + (grad_phi.dot(grad_phi)) / 2 +poisson.constitutive_model.Parameters.diffusivity = k +poisson.constitutive_model.c + + +# %% +poisson._setup_pointwise_functions() +poisson._G3 + + +# Use initial guess from linear solve + +poisson.solve(zero_init_guess=False) + +# Validate + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh2 = vis.mesh_to_pv_mesh(mesh) + pvmesh2.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh2, phi.sym) + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh( + pvmesh2, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="T", + use_transparency=False, + opacity=0.5, + # scalar_bar_args=sargs, + ) + + pl.camera_position = "xy" + + pl.show(cpos="xy") + # pl.screenshot(filename="test.png") + +# ## Analysis (Gradient recovery) +# +# We'd like to be able to look at the values of diffusivity or the +# heat flux. +# +# These are discontinuous values computed in the element interiors but can +# be projected to a `meshVariable`: +# + +# %% +projection = uw.systems.Projection(mesh, scalar) +projection.uw_function = sympy.diff(phi.sym[0], mesh.X[1]) +projection.smoothing = 1.0e-4 + +projection.solve() + + +with mesh.access(): + print(phi.stats()) + print(scalar.stats()) + +# %% +sympy.diff(scalar.sym[0], mesh.X[1]) + +# Validate + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh2 = vis.mesh_to_pv_mesh(mesh) + + pvmesh2.point_data["K"] = vis.scalar_fn_to_pv_points(pvmesh2, scalar.sym) + pvmesh2.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh2, phi.sym) + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh( + pvmesh2, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="T", + use_transparency=False, + opacity=0.5, + # scalar_bar_args=sargs, + ) + + pl.camera_position = "xy" + + pl.show(cpos="xy") + # pl.screenshot(filename="test.png") + +poisson.snes.view() + +timing.print_table() + + diff --git a/main/_sources/Notebooks/Examples-PoissonEquation/Ex_Poisson_Cartesian_Generic.py b/main/_sources/Notebooks/Examples-PoissonEquation/Ex_Poisson_Cartesian_Generic.py new file mode 100644 index 0000000..598f276 --- /dev/null +++ b/main/_sources/Notebooks/Examples-PoissonEquation/Ex_Poisson_Cartesian_Generic.py @@ -0,0 +1,135 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Poisson Equation (generic) +# +# First we show how this works using the generic class and then the minor differences for +# the `Poisson` class +# +# ## Generic scalar solver class + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +import underworld3 as uw +import numpy as np +import sympy + +from underworld3.meshing import UnstructuredSimplexBox + +mesh = UnstructuredSimplexBox( + minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0), regular=False, cellSize=1.0 / 32 +) + +t_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=1) +t_soln0 = uw.discretisation.MeshVariable("T0", mesh, 1, degree=1) + +# + +poisson0 = uw.systems.SNES_Scalar(mesh, u_Field=t_soln0) +poisson0.F0 = 0.0 +poisson0.F1 = 1.0 * poisson0.Unknowns.L +poisson0.add_dirichlet_bc(1.0, "Bottom", 0) +poisson0.add_dirichlet_bc(0.0, "Top", 0) + +poisson0.constitutive_model = uw.constitutive_models.DiffusionModel +poisson0.constitutive_model.Parameters.diffusivity = 1.0 +# - + +poisson0.solve() + +# ## `Poisson` Class +# +# Here is the other way to solve this, using the `Poisson` class which does not much +# more than add a template for the flux term. + +# + +# Create Poisson object +poisson = uw.systems.Poisson(mesh, u_Field=t_soln) +poisson.constitutive_model = uw.constitutive_models.DiffusionModel +poisson.constitutive_model.Parameters.diffusivity = 1.0 + +poisson.f = 0.0 +poisson.add_dirichlet_bc(1.0, "Bottom", 0) +poisson.add_dirichlet_bc(0.0, "Top", 0) +# - + +# Solve time +poisson.solve() + +poisson.F0 +sympy.Matrix((0,)) + +# + +# Check the flux term +display(poisson._L) + +# This is the internal build of the flux term +display(poisson._f1) +# - + +poisson.Unknowns.L + +poisson.u.sym.jacobian(poisson.Unknowns.L) + +poisson._f1.jacobian(poisson.Unknowns.L) + +# ## Validation + +# + +# Check. Construct simple linear which is solution for +# above config. Exclude boundaries from mesh data. + +import numpy as np + +with mesh.access(): + mesh_numerical_soln = uw.function.evaluate(poisson.u.fn, mesh.data) + mesh_analytic_soln = uw.function.evaluate(1.0 - mesh.N.y, mesh.data) + + if not np.allclose(mesh_analytic_soln, mesh_numerical_soln, rtol=0.001, atol=0.01): + raise RuntimeError("Unexpected values encountered.") + +# + +from mpi4py import MPI + +if MPI.COMM_WORLD.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh) + pvmesh.point_data["T"] = mesh_analytic_soln + pvmesh.point_data["T2"] = mesh_numerical_soln + pvmesh.point_data["DT"] = pvmesh.point_data["T"] - pvmesh.point_data["T2"] + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="DT", + use_transparency=False, + opacity=0.5, + ) + + pl.camera_position = "xy" + + pl.show(cpos="xy") + + +# - + + diff --git a/main/_sources/Notebooks/Examples-PoissonEquation/Ex_Poisson_Gradient_Recovery.py b/main/_sources/Notebooks/Examples-PoissonEquation/Ex_Poisson_Gradient_Recovery.py new file mode 100644 index 0000000..b9b92b0 --- /dev/null +++ b/main/_sources/Notebooks/Examples-PoissonEquation/Ex_Poisson_Gradient_Recovery.py @@ -0,0 +1,201 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Poisson Equation with flux recovery +# +# +# ## Generic scalar solver class + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +from petsc4py import PETSc +import underworld3 as uw + +import numpy as np +import sympy + +# %% +mesh = uw.meshing.UnstructuredSimplexBox( + minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0), cellSize=1.0 / 3.0, qdegree=2 +) + +mesh.dm.view() + + +# mesh variables + +t_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=3) +dTdY = uw.discretisation.MeshVariable( + r"\partial T/ \partial \mathbf{y}", mesh, 1, degree=2 +) +kappa = uw.discretisation.MeshVariable(r"\kappa", mesh, 1, degree=2) +gradT = uw.discretisation.MeshVariable( + r"\nabla\left[T\right]", mesh, mesh.dim, degree=2 +) + + +# Create Poisson object + +gradient = uw.systems.Projection(mesh, dTdY) +delT = mesh.vector.gradient(t_soln.sym) +gradient.uw_function = delT.dot(delT) +gradient.smoothing = 1.0e-3 + +# These are both SNES Scalar objects + +gradT_projector = uw.systems.Vector_Projection(mesh, gradT) +gradT_projector.uw_function = mesh.vector.gradient(t_soln.sym) +# gradT_projector.add_dirichlet_bc((0), ["Left", "Right"], components=(0)) + +# # the actual solver + +poisson = uw.systems.Poisson(mesh, u_Field=t_soln) + + +# %% +poisson.constitutive_model = uw.constitutive_models.DiffusionModel + +# Non-linear diffusivity + +delT = mesh.vector.gradient(t_soln.sym) +k = 5 + (delT.dot(delT)) / 2 + +poisson.constitutive_model.Parameters.diffusivity = k +display(poisson.constitutive_model.c) + +# projector for diffusivity (though we can just switch the rhs for the gradient object + +# + +diffusivity = uw.systems.Projection(mesh, kappa) +diffusivity.uw_function = sympy.Matrix( + [poisson.constitutive_model.Parameters.diffusivity] +) + +diffusivity.add_dirichlet_bc(k, "Bottom", components=0) +diffusivity.add_dirichlet_bc(k, "Top", components=0) +diffusivity.add_dirichlet_bc(k, "Right", components=0) +diffusivity.add_dirichlet_bc(k, "Left", components=0) + +diffusivity.smoothing = 1.0e-6 +# - + + +# %% +poisson.constitutive_model = uw.constitutive_models.DiffusionModel +poisson.constitutive_model.Parameters.diffusivity = k +poisson.constitutive_model.Parameters.diffusivity + +# %% +display(gradT_projector.uw_function) +display(diffusivity.uw_function) + +# %% +diffusivity.uw_function + +# Set some things + +x, y = mesh.X + +abs_r2 = x**2 + y**2 +poisson.f = -16 * abs_r2 +poisson.add_dirichlet_bc(abs_r2, "Bottom", components=0) +poisson.add_dirichlet_bc(abs_r2, "Top", components=0) +poisson.add_dirichlet_bc(abs_r2, "Right", components=0) +poisson.add_dirichlet_bc(abs_r2, "Left", components=0) + +# + +# %% +# Linear model - starting guess + +poisson.constitutive_model.Parameters.diffusivity = 1 +poisson.solve(zero_init_guess=True) +# - + +# %% +# Solve time +poisson.constitutive_model.Parameters.diffusivity = k +poisson.solve(zero_init_guess=False) + +# %% +poisson.constitutive_model + +# %% +gradT_projector.solve() + +# %% +gradient.uw_function = sympy.diff(t_soln.sym, mesh.N.y) +gradient.solve() + +# %% +gradient.uw_function + +# %% +diffusivity.solve() + +# non-linear smoothing term (probably not needed especially at the boundary) + +gradient.uw_function = sympy.diff(t_soln.fn, mesh.N.y) +gradient.solve(_force_setup=True) + +# %% +gradT_projector.solve() + +# **Check** Construct simple linear function which is solution for +# above config. Exclude boundaries from mesh data. + +import numpy as np + +with mesh.access(): + mesh_numerical_soln = uw.function.evaluate(t_soln.sym[0], mesh.data) + # if not np.allclose(mesh_numerical_soln, -1.0, rtol=0.01): + # raise RuntimeError("Unexpected values encountered.") + +# +# Validate + +from mpi4py import MPI + +if MPI.COMM_WORLD.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh) + pvmesh.point_data["T"] = mesh_numerical_soln + pvmesh.point_data["dTdY"] = vis.scalar_fn_to_pv_points(pvmesh, dTdY.sym) + pvmesh.point_data["dTdY1"] = vis.scalar_fn_to_pv_points(pvmesh, gradT.sym[1]) + pvmesh.point_data["dTdX1"] = vis.scalar_fn_to_pv_points(pvmesh, gradT.sym[0]) + pvmesh.point_data["kappa"] = vis.scalar_fn_to_pv_points(pvmesh, kappa.sym) + pvmesh.point_data["kappa1"] = vis.scalar_fn_to_pv_points(pvmesh, 5 + gradT.sym[0] ** 2 + gradT.sym[1] ** 2) + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="dTdX1", + use_transparency=False, + opacity=0.5, + ) + + pl.camera_position = "xy" + + pl.show(cpos="xy") + # pl.screenshot(filename="test.png") + + diff --git a/main/_sources/Notebooks/Examples-PoissonEquation/Ex_Poisson_Spherical.py b/main/_sources/Notebooks/Examples-PoissonEquation/Ex_Poisson_Spherical.py new file mode 100644 index 0000000..d970915 --- /dev/null +++ b/main/_sources/Notebooks/Examples-PoissonEquation/Ex_Poisson_Spherical.py @@ -0,0 +1,291 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Steady state diffusion in a hollow sphere + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +import pygmsh, meshio + +# + +from petsc4py import PETSc +import underworld3 as uw +from underworld3.systems import Poisson +import numpy as np +import os + +# Define the problem size +# 1 - ultra low res for automatic checking +# 2 - low res problem to play with this notebook +# 3 - medium resolution (be prepared to wait) +# 4 - highest resolution (benchmark case from Spiegelman et al) + +problem_size = 1 + +# For testing and automatic generation of notebook output, +# over-ride the problem size if the UW_TESTING_LEVEL is set + +uw_testing_level = os.environ.get("UW_TESTING_LEVEL") +if uw_testing_level: + try: + problem_size = int(uw_testing_level) + except ValueError: + # Accept the default value + pass + +# - + + +# Set some things +k = 1.0 +f = 0.0 +t_i = 2.0 +t_o = 1.0 +r_i = 0.5 +r_o = 1.0 + +# %% +from underworld3.meshing import Annulus + +# %% +# first do 2D +if problem_size <= 1: + cell_size = 0.05 +elif problem_size == 2: + cell_size = 0.02 +elif problem_size == 3: + cell_size = 0.01 +elif problem_size >= 4: + cell_size = 0.0033 + +mesh = Annulus(radiusInner=r_i, radiusOuter=r_o, cellSize=cell_size) + +t_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=2) + +# Create Poisson object +poisson = Poisson(mesh, u_Field=t_soln) +poisson.constitutive_model = uw.constitutive_models.DiffusionModel +poisson.constitutive_model.Parameters.diffusivity = 1 + +poisson.f = f + +poisson.petsc_options["snes_rtol"] = 1.0e-6 +poisson.petsc_options.delValue("ksp_monitor") +poisson.petsc_options.delValue("ksp_rtol") + + +# %% +mesh.dm.view() + +# %% +import sympy + +poisson.add_dirichlet_bc(t_i, "Lower", 0) +poisson.add_dirichlet_bc(t_o, "Upper", 0) + +# %% +poisson.solve() + +# + +# poisson.snes.view() +# - + +# %% +# Check. Construct simple solution for above config. +import math + +A = (t_i - t_o) / (sympy.log(r_i) - math.log(r_o)) +B = t_o - A * sympy.log(r_o) +sol = A * sympy.log(sympy.sqrt(mesh.N.x**2 + mesh.N.y**2)) + B + +with mesh.access(): + mesh_analytic_soln = uw.function.evaluate(sol, mesh.data, mesh.N) + mesh_numerical_soln = uw.function.evaluate(t_soln.fn, mesh.data, mesh.N) + +import numpy as np + +if not np.allclose(mesh_analytic_soln, mesh_numerical_soln, rtol=0.01): + raise RuntimeError("Unexpected values encountered.") + +# %% +poisson.constitutive_model.Parameters.diffusivity = 1.0 + 0.1 * poisson.u.fn**1.5 +poisson.f = 0.01 * poisson.u.sym[0] ** 0.5 +poisson.solve(zero_init_guess=False) + +# Validate + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh) + pvmesh.point_data["T"] = mesh_analytic_soln + pvmesh.point_data["T2"] = mesh_numerical_soln + pvmesh.point_data["DT"] = mesh_analytic_soln - mesh_numerical_soln + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="DT", + use_transparency=False, + opacity=0.5, + ) + + pl.camera_position = "xy" + + pl.show(cpos="xy") + # pl.screenshot(filename="test.png") + +# + +# %% + +expt_name = "Poisson-Annulus" +outdir = "output" +os.makedirs(f"{outdir}", exist_ok=True) + + +mesh.write_timestep( + expt_name, meshUpdates=True, meshVars=[t_soln], outputPath=outdir, index=0 +) + + +# savefile = "output/poisson_disc.h5" +# mesh.save(savefile) +# poisson.u.save(savefile) +# mesh.generate_xdmf(savefile) +# - + +# %% +from underworld3.meshing import SphericalShell +from underworld3.meshing import SegmentedSphere + +# + +# %% +# now do 3D + +problem_size = 1 + +if problem_size <= 1: + cell_size = 0.3 +elif problem_size == 1: + cell_size = 0.15 +elif problem_size == 2: + cell_size = 0.05 +elif problem_size == 3: + cell_size = 0.02 + +mesh_3d = SphericalShell( + radiusInner=r_i, + radiusOuter=r_o, + cellSize=cell_size, + refinement=1, +) + +# mesh_3d = SegmentedSphere(radiusInner=r_i, +# radiusOuter=r_o, +# cellSize=cell_size +# ) + + +t_soln_3d = uw.discretisation.MeshVariable("T", mesh_3d, 1, degree=2) +# - + +mesh_3d.dm.view() + +# Create Poisson object +poisson = Poisson(mesh_3d, u_Field=t_soln_3d) +poisson.constitutive_model = uw.constitutive_models.DiffusionModel +poisson.constitutive_model.Parameters.diffusivity = k +poisson.f = f + +poisson.petsc_options["snes_rtol"] = 1.0e-6 +poisson.petsc_options.delValue("ksp_rtol") + +import sympy + +poisson.add_dirichlet_bc(t_i, "Lower", 0) +poisson.add_dirichlet_bc(t_o, "Upper", 0) + +# Solve time +poisson.solve() + +# Check. Construct simple solution for above config. + +A = (t_i - t_o) / (1 / r_i - 1 / r_o) +B = t_o - A / r_o +sol = A / (sympy.sqrt(mesh_3d.N.x**2 + mesh_3d.N.y**2 + mesh_3d.N.z**2)) + B + +with mesh.access(): + mesh_analytic_soln = uw.function.evaluate(sol, mesh_3d.data, mesh_3d.N) + mesh_numerical_soln = uw.function.evaluate(t_soln_3d.fn, mesh_3d.data, mesh_3d.N) + +import numpy as np + +if not np.allclose(mesh_analytic_soln, mesh_numerical_soln, rtol=0.1): + raise RuntimeError("Unexpected values encountered.") + +# Validate + +from mpi4py import MPI + +if MPI.COMM_WORLD.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + + pvmesh = vis.mesh_to_pv_mesh(mesh_3d) + pvmesh.point_data["T"] = mesh_analytic_soln + pvmesh.point_data["T2"] = mesh_numerical_soln + pvmesh.point_data["DT"] = pvmesh.point_data["T"] - pvmesh.point_data["T2"] + + clipped = pvmesh.clip(origin=(0.001, 0.0, 0.0), normal=(1, 0, 0), invert=True) + + pl = pv.Plotter() + + pl.add_mesh( + clipped, + cmap="coolwarm", + edge_color="Grey", + show_edges=True, + scalars="T2", + use_transparency=False, + opacity=1.0, + ) + + pl.camera_position = "xy" + + pl.show(cpos="xy") + # pl.screenshot(filename="test.png") + +# + +# %% +expt_name = "Poisson-Sphere" +outdir = "output" +os.makedirs(f"{outdir}", exist_ok=True) + +mesh_3d.write_timestep( + expt_name, meshUpdates=True, meshVars=[t_soln_3d], outputPath=outdir, index=0 +) + +# - + + diff --git a/main/_sources/Notebooks/Examples-PoissonEquation/Readme.md b/main/_sources/Notebooks/Examples-PoissonEquation/Readme.md new file mode 100644 index 0000000..d3647d0 --- /dev/null +++ b/main/_sources/Notebooks/Examples-PoissonEquation/Readme.md @@ -0,0 +1,11 @@ +# Solving the Poisson Equation + +## Recent solver and visualization updates + +- [x] Ex_DiffusionSLCN_Cartesian-NL.py +- [x] Ex_DiffusionSLCN_Cartesian.py +- [x] Ex_Poisson_Cartesian_Generic.py +- [x] Ex_Poisson_Cartesian.py +- [x] Ex_Poisson_Gradient_Recovery.py +- [x] Ex_Poisson_Spherical.py + diff --git a/main/_sources/Notebooks/Examples-PoissonEquation/output/README.md b/main/_sources/Notebooks/Examples-PoissonEquation/output/README.md new file mode 100644 index 0000000..ae828a6 --- /dev/null +++ b/main/_sources/Notebooks/Examples-PoissonEquation/output/README.md @@ -0,0 +1,3 @@ +# Poisson model outputs + +These files are NOT under version control \ No newline at end of file diff --git a/main/_sources/Notebooks/Examples-PorousFlow/Ex_Darcy_1D_benchmark-Swarm.py b/main/_sources/Notebooks/Examples-PorousFlow/Ex_Darcy_1D_benchmark-Swarm.py new file mode 100644 index 0000000..2441e37 --- /dev/null +++ b/main/_sources/Notebooks/Examples-PorousFlow/Ex_Darcy_1D_benchmark-Swarm.py @@ -0,0 +1,272 @@ +# --- +# jupyter: +# jupytext: +# formats: py:light +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Darcy flow (1d) using swarm variable to define permeability +# +# + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +from petsc4py import PETSc +import underworld3 as uw +import numpy as np +import sympy + +options = PETSc.Options() + +# + +minX, maxX = -1.0, 0.0 +minY, maxY = -1.0, 0.0 + +mesh = uw.meshing.UnstructuredSimplexBox( + minCoords=(minX, minY), maxCoords=(maxX, maxY), cellSize=0.05, qdegree=3 +) + +# mesh = uw.meshing.StructuredQuadBox(elementRes=(20,20), +# minCoords=(minX,minY), +# maxCoords=(maxX,maxY),) + + +p_soln = uw.discretisation.MeshVariable("P", mesh, 1, degree=3) +v_soln = uw.discretisation.MeshVariable("U", mesh, mesh.dim, degree=2) + +# x and y coordinates +x = mesh.N.x +y = mesh.N.y + +# + + +if uw.mpi.size == 1: + # plot the mesh + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh) + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + use_transparency=False, + ) + + pl.show(cpos="xy") +# - + +# Create Darcy Solver +darcy = uw.systems.SteadyStateDarcy(mesh, h_Field=p_soln, v_Field=v_soln) +darcy.petsc_options.delValue("ksp_monitor") +darcy.petsc_options[ + "snes_rtol" +] = 1.0e-6 # Needs to be smaller than the contrast in properties +darcy.constitutive_model = uw.constitutive_models.DiffusionModel +darcy.constitutive_model.Parameters.diffusivity = 1 + + +# + +swarm = uw.swarm.Swarm(mesh=mesh) +material = uw.swarm.IndexSwarmVariable("M", swarm, indices=2, proxy_continuous=False) +# k = uw.swarm.IndexSwarmVariable("k", swarm, indices=2) + +swarm.populate(fill_param=2) + +# + +# Groundwater pressure boundary condition on the bottom wall + +max_pressure = 0.5 +initialPressure = -1.0 * y * max_pressure + +# + +# set up two materials +interfaceY = -0.26 + + +from sympy import Piecewise, ceiling, Abs + +k1 = 1.0 +k2 = 1.0e-4 + +# # The piecewise version +# kFunc = Piecewise((k1, y >= interfaceY), (k2, y < interfaceY), (1.0, True)) + +# darcy.constitutive_model.material_properties = darcy.constitutive_model.Parameters(diffusivity=kFunc) +# - + +with swarm.access(material): + material.data[swarm.data[:, 1] >= interfaceY] = 0 + material.data[swarm.data[:, 1] < interfaceY] = 1 + +# + +mat_k = np.array([k1, k2]) + +kFn = mat_k[0] * material.sym[0] + mat_k[1] * material.sym[1] + +darcy.constitutive_model.Parameters.diffusivity = kFn + +# + +# A smooth version +# kFunc = k2 + (k1-k2) * (0.5 + 0.5 * sympy.tanh(100.0*(y-interfaceY))) + +darcy.f = 0.0 +darcy.s = sympy.Matrix([0, -1]).T + +# set up boundary conditions +darcy.add_dirichlet_bc(0.0, "Top") +darcy.add_dirichlet_bc(-1.0 * minY * max_pressure, "Bottom") + +# Zero pressure gradient at sides / base (implied bc) + +darcy._v_projector.petsc_options["snes_rtol"] = 1.0e-6 +darcy._v_projector.smoothing = 1.0e-6 +darcy._v_projector.add_dirichlet_bc(0.0, "Left", 0) +darcy._v_projector.add_dirichlet_bc(0.0, "Right", 0) +# - +# Solve time +darcy.solve() + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh) + pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym) + pvmesh.point_data["K"] = vis.scalar_fn_to_pv_points(pvmesh, kFn) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym) + + # point sources at cell centres + points = np.zeros((mesh._centroids.shape[0], 3)) + points[:, 0] = mesh._centroids[:, 0] + points[:, 1] = mesh._centroids[:, 1] + point_cloud0 = pv.PolyData(points[::3]) + + + pvstream = pvmesh.streamlines_from_source( + point_cloud0, + vectors="V", + integrator_type=45, + integration_direction="both", + max_steps=1000, + max_time=0.25, + initial_step_length=0.001, + max_step_length=0.01, + ) + + points = vis.swarm_to_pv_cloud(swarm) + point_cloud1 = pv.PolyData(points) + point_cloud1.point_data["K"] = vis.scalar_fn_to_pv_points(point_cloud1, kFn) + + with swarm.access(): + point_cloud1.point_data["M"] = material.data.copy() + + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="P", + use_transparency=False, + opacity=1.0, + ) + + pl.add_mesh( + point_cloud1, + cmap="coolwarm", + edge_color="Black", + show_edges=False, + scalars="M", + use_transparency=False, + opacity=0.95, + ) + + pl.add_mesh(pvstream, line_width=10.0) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.005, opacity=0.75) + + pl.show(cpos="xy") + + +# + +# set up interpolation coordinates +ycoords = np.linspace(minY + 0.001 * (maxY - minY), maxY - 0.001 * (maxY - minY), 100) +xcoords = np.full_like(ycoords, -1) +xy_coords = np.column_stack([xcoords, ycoords]) + +pressure_interp = uw.function.evaluate(p_soln.sym[0], xy_coords) + + +# + +La = -1.0 * interfaceY +Lb = 1.0 + interfaceY +dP = max_pressure + +S = 1 +Pa = (dP / Lb - S + k1 / k2 * S) / (1.0 / Lb + k1 / k2 / La) +pressure_analytic = np.piecewise( + ycoords, + [ycoords >= -La, ycoords < -La], + [ + lambda ycoords: -Pa * ycoords / La, + lambda ycoords: Pa + (dP - Pa) * (-ycoords - La) / Lb, + ], +) + +S = 0 +Pa = (dP / Lb - S + k1 / k2 * S) / (1.0 / Lb + k1 / k2 / La) +pressure_analytic_noG = np.piecewise( + ycoords, + [ycoords >= -La, ycoords < -La], + [ + lambda ycoords: -Pa * ycoords / La, + lambda ycoords: Pa + (dP - Pa) * (-ycoords - La) / Lb, + ], +) + +# + +import matplotlib.pyplot as plt + +# %matplotlib inline + +fig = plt.figure() +ax1 = fig.add_subplot(111, xlabel="Pressure", ylabel="Depth") +ax1.plot(pressure_interp, ycoords, linewidth=3, label="Numerical solution") +ax1.plot( + pressure_analytic, ycoords, linewidth=3, linestyle="--", label="Analytic solution" +) +ax1.plot( + pressure_analytic_noG, + ycoords, + linewidth=3, + linestyle="--", + label="Analytic (no gravity)", +) +ax1.grid("on") +ax1.legend() +# - + + diff --git a/main/_sources/Notebooks/Examples-PorousFlow/Ex_Darcy_1D_benchmark.py b/main/_sources/Notebooks/Examples-PorousFlow/Ex_Darcy_1D_benchmark.py new file mode 100644 index 0000000..bfaddb3 --- /dev/null +++ b/main/_sources/Notebooks/Examples-PorousFlow/Ex_Darcy_1D_benchmark.py @@ -0,0 +1,223 @@ +# --- +# jupyter: +# jupytext: +# formats: py:light +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Darcy flow (1d) using xy coordinates to define permeability distribution + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +from petsc4py import PETSc +import underworld3 as uw +import numpy as np +import sympy + +options = PETSc.Options() + +# + +minX, maxX = -1.0, 0.0 +minY, maxY = -1.0, 0.0 + +mesh = uw.meshing.UnstructuredSimplexBox( + minCoords=(minX, minY), maxCoords=(maxX, maxY), cellSize=0.05, qdegree=3 +) + + +# x and y coordinates +x = mesh.N.x +y = mesh.N.y + +# + +# Create Darcy Solver +darcy = uw.systems.SteadyStateDarcy(mesh) + +p_soln = darcy.Unknowns.u +v_soln = darcy.v + +darcy.petsc_options[ + "snes_rtol" +] = 1.0e-6 # Needs to be smaller than the contrast in properties + +darcy.constitutive_model = uw.constitutive_models.DarcyFlowModel +darcy.constitutive_model.Parameters.permeability = 1 +# - + + +p_soln_0 = p_soln.clone("P_no_g", r"{p_\textrm{no g}}") +v_soln_0 = v_soln.clone("V_no_g", r"{v_\textrm{no g}}") + +# + +# Groundwater pressure boundary condition on the bottom wall + +max_pressure = 0.5 + +# + +# set up two materials + +interfaceY = -0.26 + +from sympy import Piecewise, ceiling, Abs + +k1 = 1.0 +k2 = 1.0e-4 + +# The piecewise version +kFunc = Piecewise((k1, y >= interfaceY), (k2, y < interfaceY), (1.0, True)) + +# A smooth version + +darcy.constitutive_model.Parameters.permeability = kFunc +darcy.constitutive_model.Parameters.s = sympy.Matrix([0, 0]).T +darcy.f = 0.0 + +# set up boundary conditions +darcy.add_dirichlet_bc(0.0, "Top") +darcy.add_dirichlet_bc(-1.0 * minY * max_pressure, "Bottom") + +# - +# Solve time +darcy.solve() +with mesh.access(p_soln_0, v_soln_0): + p_soln_0.data[...] = p_soln.data[...] + v_soln_0.data[...] = v_soln.data[...] + + +# + +# now switch on gravity + +darcy.constitutive_model.Parameters.s = sympy.Matrix([0, -1]).T +darcy.solve() + +# - + + + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh) + pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym) + pvmesh.point_data["K"] = vis.scalar_fn_to_pv_points(pvmesh, kFunc) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym) + + # point sources at cell centres + points = np.zeros((mesh._centroids.shape[0], 3)) + points[:, 0] = mesh._centroids[:, 0] + points[:, 1] = mesh._centroids[:, 1] + point_cloud = pv.PolyData(points[::3]) + + pvstream = pvmesh.streamlines_from_source( + point_cloud, + vectors="V", + integrator_type=45, + integration_direction="both", + max_steps=1000, + max_time=0.1, + initial_step_length=0.001, + max_step_length=0.01, + ) + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="P", + use_transparency=False, + opacity=1.0, + ) + + pl.add_mesh(pvstream, line_width=1.0) + + # pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.005, opacity=0.75) + + pl.show(cpos="xy") + + +# + +# set up interpolation coordinates +ycoords = np.linspace(minY + 0.001 * (maxY - minY), maxY - 0.001 * (maxY - minY), 100) +xcoords = np.full_like(ycoords, -1) +xy_coords = np.column_stack([xcoords, ycoords]) + +pressure_interp = uw.function.evalf(p_soln.sym[0], xy_coords) +pressure_interp_0 = uw.function.evalf(p_soln_0.sym[0], xy_coords) + + +# + +La = -1.0 * interfaceY +Lb = 1.0 + interfaceY +dP = max_pressure + +S = 1 +Pa = (dP / Lb - S + k1 / k2 * S) / (1.0 / Lb + k1 / k2 / La) +pressure_analytic = np.piecewise( + ycoords, + [ycoords >= -La, ycoords < -La], + [ + lambda ycoords: -Pa * ycoords / La, + lambda ycoords: Pa + (dP - Pa) * (-ycoords - La) / Lb, + ], +) + +S = 0 +Pa = (dP / Lb - S + k1 / k2 * S) / (1.0 / Lb + k1 / k2 / La) +pressure_analytic_noG = np.piecewise( + ycoords, + [ycoords >= -La, ycoords < -La], + [ + lambda ycoords: -Pa * ycoords / La, + lambda ycoords: Pa + (dP - Pa) * (-ycoords - La) / Lb, + ], +) + +# + +import matplotlib.pyplot as plt + +# %matplotlib inline + +fig = plt.figure() +ax1 = fig.add_subplot(111, xlabel="Pressure", ylabel="Depth") +ax1.plot(pressure_interp, ycoords, linewidth=3, label="Numerical solution") +ax1.plot(pressure_interp_0, ycoords, linewidth=3, label="Numerical solution (no G)") +ax1.plot( + pressure_analytic, ycoords, linewidth=3, linestyle="--", label="Analytic solution" +) +ax1.plot( + pressure_analytic_noG, + ycoords, + linewidth=3, + linestyle="--", + label="Analytic (no gravity)", +) +ax1.grid("on") +ax1.legend() +# - +darcy.view() + + +darcy.darcy_flux + + + + diff --git a/main/_sources/Notebooks/Examples-PorousFlow/Ex_Darcy_Cartesian.py b/main/_sources/Notebooks/Examples-PorousFlow/Ex_Darcy_Cartesian.py new file mode 100644 index 0000000..ce66659 --- /dev/null +++ b/main/_sources/Notebooks/Examples-PorousFlow/Ex_Darcy_Cartesian.py @@ -0,0 +1,173 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Underworld Groundwater Flow Benchmark 1 +# +# See the Underworld2 example by Adam Beall. +# +# Flow driven by gravity and topography. We check the flow for constant permeability and for exponentially decreasing permeability as a function of depth. +# +# *Note*, this benchmark is a bit problematic because the surface shape is not really +# consistent with the sidewall boundary conditions - zero gradients at the vertical boundaries.If we replace the sin(x) term with cos(x) to describe the surface then it works a little better because there is no kink in the surface topography at the walls. +# +# *Note*, there is not an obvious way in pyvista to make the streamlines smaller / shorter / fainter where flow rates are very low so the visualisation is a little misleading right now. +# + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# %% +from petsc4py import PETSc +import underworld3 as uw +import numpy as np +import sympy + +options = PETSc.Options() + +# %% +mesh = uw.meshing.UnstructuredSimplexBox( + minCoords=(0.0, 0.0), maxCoords=(4.0, 1.0), cellSize=0.05, qdegree=3 +) + +p_soln = uw.discretisation.MeshVariable("P", mesh, 1, degree=2) +v_soln = uw.discretisation.MeshVariable("U", mesh, mesh.dim, degree=1, continuous=False) + + +# Mesh deformation + +x, y = mesh.X + +h_fn = 1.0 + x * 0.2 / 4 + 0.04 * sympy.cos(2.0 * np.pi * x) * y + +new_coords = mesh.data.copy() +new_coords[:, 1] = uw.function.evaluate(h_fn * y, mesh.data, mesh.N) + +mesh.deform_mesh(new_coords=new_coords) + + + +# %% +if uw.mpi.size == 1 and uw.is_notebook: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh) + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + use_transparency=False, + ) + + pl.show(cpos="xy") + +# %% +# Create Poisson object +darcy = uw.systems.SteadyStateDarcy(mesh, h_Field=p_soln, v_Field=v_soln) +darcy.constitutive_model = uw.constitutive_models.DarcyFlowModel +darcy.constitutive_model.Parameters.permeability = 1 +darcy.petsc_options.delValue("ksp_monitor") + +# Set some things + +k = sympy.exp(-2.0 * 2.302585 * (h_fn - y)) # powers of 10 +darcy.constitutive_model.Parameters.permeability = k + +k + +darcy.f = 0.0 +darcy.constitutive_model.Parameters.s = sympy.Matrix([0, -1]).T + +darcy.add_dirichlet_bc(0.0, "Top") + +# Zero pressure gradient at sides / base (implied bc) + +darcy._v_projector.smoothing = 0.0 + + + +# %% +# Solve time +darcy.petsc_options.setValue("snes_monitor", None) +darcy.solve(verbose=False) + +# %% +if uw.mpi.size == 1 and uw.is_notebook: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) + + pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym) + pvmesh.point_data["dP"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym[0] - (h_fn - y)) + pvmesh.point_data["K"] = vis.scalar_fn_to_pv_points(pvmesh, k) + pvmesh.point_data["S"] = vis.scalar_fn_to_pv_points(pvmesh, sympy.log(v_soln.sym.dot(v_soln.sym))) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym) + + # point sources at cell centres + + points = np.zeros((mesh._centroids.shape[0], 3)) + points[:, 0] = mesh._centroids[:, 0] + points[:, 1] = mesh._centroids[:, 1] + point_cloud = pv.PolyData(points[::3]) + + pvstream = pvmesh.streamlines_from_source( + point_cloud, + vectors="V", + integrator_type=45, + integration_direction="both", + max_steps=1000, + max_time=0.2, + initial_step_length=0.001, + max_step_length=0.01, + ) + + pl = pv.Plotter() + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="P", + use_transparency=False, + opacity=1.0, + ) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.5, opacity=0.5) + pl.add_mesh(pvstream, line_width=1.0) + pl.show(cpos="xy") + +# +# ## Metrics + +_, _, _, max_p, _, _, _ = p_soln.stats() + + +# + + +print("Max pressure : {:4f}".format(max_p)) +# - + + diff --git a/main/_sources/Notebooks/Examples-PorousFlow/Ex_viscousFingering-Viz.py b/main/_sources/Notebooks/Examples-PorousFlow/Ex_viscousFingering-Viz.py new file mode 100644 index 0000000..cdc031b --- /dev/null +++ b/main/_sources/Notebooks/Examples-PorousFlow/Ex_viscousFingering-Viz.py @@ -0,0 +1,143 @@ +# --- +# jupyter: +# jupytext: +# formats: py:light +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# ## Viscous fingering model +# +# Based on Darcy flow and advection-diffusion of two fluids with varying viscosity. +# +# From [Guy Simpson - Practical Finite Element Modeling in Earth Science using Matlab (2017)](https://www.wiley.com/en-au/Practical+Finite+Element+Modeling+in+Earth+Science+using+Matlab-p-9781119248620) +# +# - Section 10.2 of the book +# +# #### Darcy pressure solution (quasi-static) +# +# $$\nabla \cdot \left( \boldsymbol\kappa \nabla p - \boldsymbol{s} \right) + W = 0$$ +# +# #### Darcy velocity: +# $$u = - \frac{k}{\mu_c}\nabla p$$ +# +# $$\nabla \cdot \mathbf{u} = 0$$ +# +# +# ### viscosity: +# $$\mu_c = \left( \frac{c}{\mu_o^{\frac{1}{4}}} + \frac{1-c}{\mu_s^{\frac{1}{4}}} \right)^{-4}$$ +# +# #### Advection-diffusion of material type (solvent / oil): +# +# $$\varphi \frac{\partial c}{\partial t} + \mathbf{u} \cdot \nabla c = \nabla(\kappa\nabla c)$$ +# +# +# ##### Model physical parameters: +# +# | parameter | symbol | value | units | | +# |---|---|---|---|---| +# | x | | $$10$$ | $$m$$ | | +# | y | | $$10$$ | $$m$$ | | +# | permeability | $$k$$ | $$10^{-13}$$ | $$m^2$$ | | +# | porosity | $$\varphi$$ | $$0.1$$ | | | +# | diffusivity | $$\kappa$$ | $$10^{-9}$$ | $$m^2 s^{-1}$$ | | +# | viscosity (solvent) | $$\eta{_s}$$ | $$1.33{\cdot}10^{-4}$$ | $$Pa s$$ | | +# | viscosity (oil) | $$\eta{_o}$$ | $$20\eta_s$$ | $$Pa s$$ | | +# | pressure | $$p$$ | $$10^{5}$$ | $$Pa$$ | | +# + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +from petsc4py import PETSc +import underworld3 as uw +import numpy as np +import sympy + +from scipy.interpolate import griddata, interp1d + +import matplotlib.pyplot as plt + +import os + + +# + language="sh" +# +# ls -trl /Users/lmoresi/+Simulations/PorousFlow/viscousFingering_example_8/* | tail -10 + +# + +## Reading the checkpoints back in ... + +step = 95 + +checkpoint_dir = "/Users/lmoresi/+Simulations/PorousFlow/viscousFingering_example_7" +checkpoint_base = "simpson_ViscousFinger" +base_filename = os.path.join(checkpoint_dir, checkpoint_base) + +# + +mesh = uw.discretisation.Mesh(f"{base_filename}.mesh.00000.h5") + +x,y = mesh.X + +minX = mesh.data[:,0].min() +minY = mesh.data[:,1].min() +maxX = mesh.data[:,0].max() +maxY = mesh.data[:,1].max() + +v_soln_ckpt = uw.discretisation.MeshVariable("U", mesh, mesh.dim, degree=1) +p_soln_ckpt = uw.discretisation.MeshVariable("P", mesh, 1, degree=2) +mat_ckpt = uw.discretisation.MeshVariable("omega", mesh, 1, degree=3) + +vizmesh = uw.meshing.UnstructuredSimplexBox( + minCoords=(minX, minY), maxCoords=(maxX, maxY), cellSize=maxY/300, qdegree=1) + + +v_soln_ckpt.read_timestep(checkpoint_base, "U", step, outputPath=checkpoint_dir) +p_soln_ckpt.read_timestep(checkpoint_base, "P", step, outputPath=checkpoint_dir) +mat_ckpt.read_timestep(checkpoint_base, "mat", step, outputPath=checkpoint_dir) + + +# + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(vizmesh) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln_ckpt.sym) + pvmesh.point_data["mat"] = vis.scalar_fn_to_pv_points(pvmesh, mat_ckpt.sym) + pvmesh.point_data["p"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln_ckpt.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln_ckpt) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln_ckpt.sym)/vis.vector_fn_to_pv_points(velocity_points, v_soln_ckpt.sym).max() + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh(pvmesh, style="wireframe", cmap="RdYlBu_r", edge_color="Grey", scalars="mat", + show_edges=True, line_width=0.05, use_transparency=False, opacity=1) + +# pl.add_mesh(pvmesh, cmap="RdYlBu_r", edge_color="Grey", scalars="mat", +# show_edges=False, use_transparency=False, opacity=0.5) + + + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=250, opacity=1) + + + pl.show(cpos="xy") +# - + +I = uw.maths.Integral(mesh, sympy.sqrt(v_soln_ckpt.sym.dot(v_soln_ckpt.sym))) +Vrms = I.evaluate() +I.fn = 1.0 +Vrms /= I.evaluate() +Vrms diff --git a/main/_sources/Notebooks/Examples-PorousFlow/Ex_viscousFingering.py b/main/_sources/Notebooks/Examples-PorousFlow/Ex_viscousFingering.py new file mode 100644 index 0000000..3f121a5 --- /dev/null +++ b/main/_sources/Notebooks/Examples-PorousFlow/Ex_viscousFingering.py @@ -0,0 +1,431 @@ +# --- +# jupyter: +# jupytext: +# formats: py:light +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# ## Viscous fingering model +# +# Based on Darcy flow and advection-diffusion of two fluids with varying viscosity. +# From Simpson, 2017, and from Homsy, 1987, fingering patterns develop under certain conditions. +# +# ### Darcy model (quasi-static) +# +# The Darcy equation for the steady-state pressure field can be written +# +# $$\nabla \cdot \left( \boldsymbol\kappa \nabla p - \boldsymbol{s} \right) = 0$$ +# +# #### Darcy velocity: +# +# The model from Homsy (1987) generally assumes $\nabla \cdot \mathbf{u} = 0$ which is equivalent to the Darcy flow equation with $\boldsymbol{s}=0$ +# and using +# +# $$\mathbf{u} = - \frac{k}{\mu_c}\nabla p$$ +# +# +# #### viscosity: +# $$\mu_c = \left( \frac{c}{\mu_o^{{1}/{4}}} + \frac{1-c}{\mu_s^{{1}/{4}}} \right)^{-4}$$ +# +# #### Advection-diffusion of material: +# +# $$\varphi \frac{\partial c}{\partial t} + \varphi (\mathbf{u} \cdot \nabla) c= \nabla(\kappa\nabla c)$$ +# +# If the diffusion coefficient is small, then it is often more appropriate to assume pure transport +# +# $$\varphi \frac{D c}{D t} = \nabla(\kappa\nabla c) \approx 0$$ +# +# ##### Model physical parameters: +# +# | parameter | symbol | value | units | | +# |---|---|---|---|---| +# | x | | $$10$$ | $$m$$ | | +# | y | | $$10$$ | $$m$$ | | +# | permeability | $$k$$ | $$10^{-13}$$ | $$m^2$$ | | +# | porosity | $$\varphi$$ | $$0.1$$ | | | +# | diffusivity | $$\kappa$$ | $$10^{-9}$$ | $$m^2 s^{-1}$$ | | +# | viscosity (solvent) | $$\mu{_s}$$ | $$1.33{\cdot}10^{-4}$$ | $$Pa s$$ | | +# | viscosity (oil) | $$\mu{_o}$$ | $$20\eta_s$$ | $$Pa s$$ | | +# | pressure | $$p$$ | $$10^{5}$$ | $$Pa$$ | | +# +# +# ## References +# +# Homsy, G. M. (1987). Viscous Fingering in Porous Media. Annual Review of Fluid Mechanics, 19(1), 271–311. https://doi.org/10.1146/annurev.fl.19.010187.001415 +# +# [Guy Simpson - Practical Finite Element Modeling in Earth Science using Matlab (2017)](https://www.wiley.com/en-au/Practical+Finite+Element+Modeling+in+Earth+Science+using+Matlab-p-9781119248620) +# +# +# + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +from petsc4py import PETSc +import underworld3 as uw +import numpy as np +import sympy + +# from scipy.interpolate import griddata, interp1d + +import matplotlib.pyplot as plt + +import os + +options = PETSc.Options() + +# + +outputDir = "./output/viscousFingering_example/" + +if uw.mpi.rank == 0: + ### create folder if required + os.makedirs(outputDir, exist_ok=True) + +# + +# import unit registry to make it easy to convert between units +u = uw.scaling.units + +### make scaling easier +ndim, nd = uw.scaling.non_dimensionalise, uw.scaling.non_dimensionalise +dim = uw.scaling.dimensionalise + +refLength = 10 ### length and height of box in meters +g = 9.81 +eta = 1.33e-4 +kappa = 1e-9 ### m^2/s +perm = 1e-13 ### m^2 +porosity = 0.1 +T_0 = 273.15 +T_1 = 1573.15 +dT = T_1 - T_0 +rho0 = 1e3 + + +refTime = perm / kappa +refViscosity = eta * u.pascal * u.second + +KL = refLength * u.meter +KL = 1.0 * u.millimetre +KT = dT * u.kelvin +Kt = refTime * u.seconds +Kt = 0.01 * u.year +KM = refViscosity * KL * Kt + +### create unit registry +scaling_coefficients = uw.scaling.get_coefficients() +scaling_coefficients["[length]"] = KL +scaling_coefficients["[time]"] = Kt +scaling_coefficients["[mass]"] = KM +scaling_coefficients["[temperature]"] = KT +scaling_coefficients + +# + +minX, maxX = 0, nd(10 * u.meter) +minY, maxY = 0, nd(10 * u.meter) + +elements = 25 + +mesh = uw.meshing.UnstructuredSimplexBox( + minCoords=(minX, minY), maxCoords=(maxX, maxY), cellSize=maxY / elements, qdegree=5 +) + +vizmesh = uw.meshing.UnstructuredSimplexBox( + minCoords=(minX, minY), maxCoords=(maxX, maxY), cellSize=0.5 * maxY / elements, qdegree=1 +) + +p_soln = uw.discretisation.MeshVariable("P", mesh, 1, degree=2) +v_soln = uw.discretisation.MeshVariable("U", mesh, mesh.dim, degree=1) +mat = uw.discretisation.MeshVariable("mat", mesh, 1, degree=3, continuous=True) + +# x and y coordinates +x = mesh.N.x +y = mesh.N.y + + +# + + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh) + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + use_transparency=False, + ) + + pl.show(cpos="xy") + +# + +# Create Darcy Solver + +darcy = uw.systems.SteadyStateDarcy(mesh, h_Field=p_soln, v_Field=v_soln) +darcy.petsc_options.delValue("ksp_monitor") +darcy.petsc_options[ + "snes_rtol" +] = 1.0e-6 # Needs to be smaller than the contrast in properties +darcy.constitutive_model = uw.constitutive_models.DiffusionModel +# - + + +darcy + +# +# $$ +# \color{Green}{\mathbf{f}_{0}} - +# \nabla \cdot +# \color{Blue}{{\mathbf{f}_{1}}} = +# \color{Maroon}{\underbrace{\Bigl[ W \Bigl] }_{\mathbf{f}_{s}}} +# $$ +# + +# + +swarm = uw.swarm.Swarm(mesh=mesh, recycle_rate=5) + +material = swarm.add_variable(name="M", size=1, proxy_degree=mat.degree) +conc = swarm.add_variable(name="C", size=1, proxy_degree=mat.degree) + +swarm.populate(fill_param=4) + +# + +adv_diff = uw.systems.AdvDiffusionSLCN( + mesh=mesh, u_Field=mat, V_fn=v_soln, #DuDt=conc.sym[0] +) + +adv_diff.constitutive_model = uw.constitutive_models.DiffusionModel +# - + +# ### Random material distribution along the interface + +# + +np.random.seed(100) + +### on the mesh + +with mesh.access(mat): + x0 = nd(2.5 * u.meter) + dx = max(mesh.get_min_radius(), nd(0.1 * u.meter)) + + fluctuation = nd(0.01 * u.meter) * np.cos( + mat.coords[:, 1] / nd(0.5 * u.meter) * np.pi + ) + fluctuation += nd(0.01 * u.meter) * np.cos( + mat.coords[:, 1] / nd(2.0 * u.meter) * np.pi + ) + fluctuation += nd(0.05 * u.meter) * np.random.random(size=mat.coords.shape[0]) + + mat.data[...] = 0 + mat.data[mat.coords[:, 0] + fluctuation < x0] = 1 + +# ### on the swarm + +with swarm.access(material): + # material.data[:,0] = mat.rbf_interpolate(new_coords=material.swarm.data, nnn=1)[:,0] + material.data[:, 0] = uw.function.evalf(mat.sym, swarm.particle_coordinates.data) + +# - +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh) + pvmesh.point_data["mat"] = vis.scalar_fn_to_pv_points(pvmesh, mat.sym) + + + points = vis.swarm_to_pv_cloud(swarm) + point_cloud = pv.PolyData(points) + + with swarm.access(material): + point_cloud.point_data["M"] = material.data.copy() + + pl = pv.Plotter(window_size=(750, 750)) + + # pl.add_mesh(pvmesh, cmap="coolwarm", edge_color="Black", show_edges=True, use_transparency=False) + + pl.add_points( + point_cloud, + cmap="coolwarm", + render_points_as_spheres=True, + point_size=10, + opacity=0.33, + ) + + pl.show(cpos="xy") + + +eta_s = nd(1.33e-4 * u.pascal * u.second) +eta_o = 20 * eta_s + +# + +### use the mesh var to map composition to viscosity +## eta_fn = (mat.sym[0]/eta_s**0.25+(1-mat.sym[0])/eta_o**0.25)**(-4) + +### use the swarm var to map composition to viscosity +eta_fn = (material.sym[0] / eta_s**0.25 + (1 - material.sym[0]) / eta_o**0.25) ** ( + -4 +) + + +# + +nd_perm = nd(perm * u.meter**2) + +diffusivity_fn = nd_perm / eta_fn + +darcy.constitutive_model.Parameters.diffusivity = diffusivity_fn +# - + +# #### Darcy velocity: +# $$ u = - \frac{k}{\mu_c}\nabla p$$ + +adv_diff.constitutive_model.Parameters.diffusivity = nd(1e-9 * u.meter**2 / u.second) + +# + +p0_nd = nd(0.1e6 * u.pascal) +# p_dx = p0_nd * (1 - mesh.X[0]) + +# with mesh.access(p_soln): +# p_soln.data[:,0] = uw.function.evaluate(p_dx, p_soln.coords, mesh.N) + +# + +## Make sure additional terms are set to zero +darcy.f = 0.0 +darcy.s = sympy.Matrix([0, 0]).T + +### set up boundary conditions for the Darcy solver +darcy.add_dirichlet_bc(p0_nd, "Left") +darcy.add_dirichlet_bc(0.0, "Right") + +### set up boundary conditions for the adv diffusion solver +adv_diff.add_dirichlet_bc(1.0, "Left") +adv_diff.add_dirichlet_bc(0.0, "Right") + +# Zero pressure gradient at sides / base (implied bc) + +# darcy._v_projector.petsc_options["snes_rtol"] = 1.0e-6 +# darcy._v_projector.smoothing = 1e24 +# darcy._v_projector.add_dirichlet_bc(0.0, "Left", 0) +# darcy._v_projector.add_dirichlet_bc(0.0, "Right", 0) +# - +darcy.solve() + +time = 0 +step = 0 + + +# + +finish_time = 0.01 * u.year + +# while time < nd(finish_time): + +for iteration in range(0, 20): + if uw.mpi.rank == 0: + print(f"\n\nstep: {step}, time: {dim(time, u.year)}") + + if step % 5 == 0: + mesh.write_timestep( + "viscousFinger", + meshUpdates=False, + meshVars=[p_soln, v_soln, mat], + outputPath=outputDir, + index=step, + ) + + ### get the Darcy velocity from the darcy solve + darcy.solve(zero_init_guess=True) + + dt = ndim(0.0002 * u.year) + + ### do the advection-diffusion + # adv_diff.solve(timestep=dt) + + ### update swarm / swarm variables + # with swarm.access(material): + # material.data[:,0] = mat.rbf_interpolate(new_coords=material.swarm.data, nnn=1)[:,0] + # material.data[:,0] = uw.function.evaluate(mat.sym, swarm.particle_coordinates.data) + + ### advect the swarm + swarm.advection( + V_fn=v_soln.sym + * sympy.Matrix.diag(1 / porosity, 1 / sympy.sympify(1000000000)), + delta_t=dt, + order=2, + evalf=True, + ) + + # with mesh.access(v_soln): + # ## Divide by the porosity to get the actual velocity + # v_soln.data[:,] *= porosity + + I = uw.maths.Integral(mesh, sympy.sqrt(v_soln.sym.dot(v_soln.sym))) + Vrms = I.evaluate() + I.fn = 1.0 + Vrms /= I.evaluate() + + if uw.mpi.rank == 0: + print(f"V_rms = {Vrms} ... delta t = {dt}. dL = {Vrms * dt}") + + step += 1 + time += dt + + if time > nd(finish_time): + break + + +# - + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + + pvmesh = vis.mesh_to_pv_mesh(vizmesh) + pvmesh.point_data["mat"] = vis.scalar_fn_to_pv_points(pvmesh, material.sym) + pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym)/vis.vector_fn_to_pv_points(velocity_points, v_soln.sym).max() + + points = vis.swarm_to_pv_cloud(swarm) + point_cloud = pv.PolyData(points) + + with swarm.access(material): + point_cloud.point_data["M"] = material.data.copy() + + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh(pvmesh, style="surface", cmap="coolwarm", edge_color="Grey", scalars="P", + show_edges=False, use_transparency=False, opacity=1) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=1250, opacity=1) + + pl.add_points( + point_cloud, + cmap="coolwarm", + render_points_as_spheres=False, + point_size=2, + opacity=0.66, + ) + + pl.show(cpos="xy") + +mat.stats() diff --git a/main/_sources/Notebooks/Examples-PorousFlow/Readme.md b/main/_sources/Notebooks/Examples-PorousFlow/Readme.md new file mode 100644 index 0000000..35adaf4 --- /dev/null +++ b/main/_sources/Notebooks/Examples-PorousFlow/Readme.md @@ -0,0 +1,11 @@ +# Porous flow examples + +e.g. groundwater equations / Darcy flow + +## Recent solver and visualization updates + +- [ ] Ex_Darcy_Cartesian.py +- [ ] Ex_Darcy_1D_benchmark-Swarm.py +- [ ] Ex_Darcy_1D_benchmark.py +- [ ] Ex_viscousFingering.py +- [ ] Ex_viscousFingering-Viz.py diff --git a/main/_sources/Notebooks/Examples-PorousFlow/output/README.md b/main/_sources/Notebooks/Examples-PorousFlow/output/README.md new file mode 100644 index 0000000..a4c9183 --- /dev/null +++ b/main/_sources/Notebooks/Examples-PorousFlow/output/README.md @@ -0,0 +1,3 @@ +# Porous flow model outputs + +These files are NOT under version control \ No newline at end of file diff --git a/main/_sources/Notebooks/Examples-PostProcessing/Ex_ChannelFlow_Visualise.py b/main/_sources/Notebooks/Examples-PostProcessing/Ex_ChannelFlow_Visualise.py new file mode 100644 index 0000000..6fa6086 --- /dev/null +++ b/main/_sources/Notebooks/Examples-PostProcessing/Ex_ChannelFlow_Visualise.py @@ -0,0 +1,145 @@ +# --- +# jupyter: +# jupytext: +# formats: py:percent +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# %% +# Visualise Channel Flow model + + +import nest_asyncio +nest_asyncio.apply() +import os + +import petsc4py +import underworld3 as uw +import numpy as np +import sympy + + + + + + +# %% +# ls -trl ../Examples-StokesFlow/output/ChannelFlow3D + +# %% +checkpoint_dir = "../Examples-StokesFlow/output/ChannelFlow3D" +checkpoint_base = f"WigglyBottom_20" +meshfile = os.path.join(checkpoint_dir, checkpoint_base) + ".mesh.00000.h5" + +step = 0 + +# %% +uw.utilities.h5_scan("../Examples-StokesFlow/output/ChannelFlow3D/WigglyBottom_20.mesh.P1.00000.h5") + +# %% +terrain_mesh = uw.discretisation.Mesh(meshfile) + +v_soln_ckpt = uw.discretisation.MeshVariable("U1", terrain_mesh, terrain_mesh.dim, degree=2) +p_soln_ckpt = uw.discretisation.MeshVariable("P1", terrain_mesh, 1, degree=1) + +v_soln_ckpt.read_timestep(checkpoint_base, "U1", step, outputPath=checkpoint_dir) +p_soln_ckpt.read_timestep(checkpoint_base, "P1", step, outputPath=checkpoint_dir) + + +# %% +## Visualise the mesh + +# OR +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + v = v_soln_ckpt + p = p_soln_ckpt + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(terrain_mesh) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(v) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v.sym) + + clipped = pvmesh.clip(origin=(0.0, 0.0, -0.09), normal=(0.0, 0, 1), invert=True) + clipped.point_data["V"] = vis.vector_fn_to_pv_points(clipped, v.sym) + + clipped2 = pvmesh.clip(origin=(0.0, 0.0, -0.05), normal=(0.0, 0, 1), invert=True) + clipped2.point_data["V"] = vis.vector_fn_to_pv_points(clipped2, v.sym) + + clipped3 = pvmesh.clip(origin=(0.0, 0.0, 0.4), normal=(0.0, 0, 1), invert=False) + clipped3.point_data["V"] = vis.vector_fn_to_pv_points(clipped3, v.sym) + + + skip = 10 + points = np.zeros((terrain_mesh._centroids[::skip].shape[0], 3)) + points[:, 0] = terrain_mesh._centroids[::skip, 0] + points[:, 1] = terrain_mesh._centroids[::skip, 1] + points[:, 2] = terrain_mesh._centroids[::skip, 2] + + point_cloud = pv.PolyData(points[np.logical_and(points[:, 0] < 2.0, points[:, 0] > 0.0)] ) + + pvstream = pvmesh.streamlines_from_source( + point_cloud, vectors="V", + integration_direction="forward", + integrator_type=45, + surface_streamlines=False, + initial_step_length=0.1, + max_time=0.5, + max_steps=1000 + ) + + point_cloud2 = pv.PolyData(points[np.logical_and(points[:, 2] < 0.5, points[:, 2] > 0.45)] ) + + pvstream2 = pvmesh.streamlines_from_source( + point_cloud2, vectors="V", + integration_direction="forward", + integrator_type=45, + surface_streamlines=False, + initial_step_length=0.01, + max_time=0.5, + max_steps=1000 + ) + + pl = pv.Plotter(window_size=[1000, 1000]) + pl.add_axes() + + pl.add_mesh(pvmesh,'Grey', 'wireframe', opacity=0.1) + pl.add_mesh(clipped,'Blue', show_edges=False, opacity=0.25) + # pl.add_mesh(pvmesh, 'white', show_edges=True, opacity=0.5) + + #pl.add_mesh(pvstream) + pl.add_mesh(pvstream2) + + + arrows = pl.add_arrows(clipped2.points, clipped2.point_data["V"], + show_scalar_bar = False, opacity=1, + mag=100, ) + + # arrows = pl.add_arrows(clipped3.points, clipped3.point_data["V"], + # show_scalar_bar = False, opacity=1, + # mag=33, ) + + + # pl.screenshot(filename="sphere.png", window_size=(1000, 1000), return_img=False) + # OR + + pl.show(cpos="xy") + + + + +# %% diff --git a/main/_sources/Notebooks/Examples-PostProcessing/Ex_DiscStokes_Visualise.py b/main/_sources/Notebooks/Examples-PostProcessing/Ex_DiscStokes_Visualise.py new file mode 100644 index 0000000..1f179b7 --- /dev/null +++ b/main/_sources/Notebooks/Examples-PostProcessing/Ex_DiscStokes_Visualise.py @@ -0,0 +1,190 @@ +# ## Visualise circular stokes model (flow etc) + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() +import os + +import petsc4py +import underworld3 as uw +import numpy as np +import sympy + +# ls -trl ../Examples-Convection/output/Disc_Ra1e7_H1_deleta_1000.0*T* | tail + +# ls -trl /Users/lmoresi/+Simulations/InnerCore/ConvectionDisk | tail + +###### checkpoint_dir = "../Examples-Convection/output" +checkpoint_dir = "/Users/lmoresi/+Simulations/InnerCore/ConvectionDisk" +checkpoint_base = f"Disc_Ra1e7_H1_deleta_1000.0" +meshfile = os.path.join(checkpoint_dir, checkpoint_base) + ".mesh.05100.h5" + + +# + +discmesh = uw.discretisation.Mesh(meshfile, + coordinate_system_type=uw.coordinates.CoordinateSystemType.CYLINDRICAL2D) + +x = discmesh.N.x +y = discmesh.N.y + +r = sympy.sqrt(x**2 + y**2) # cf radius_fn which is 0->1 +th = sympy.atan2(y + 1.0e-5, x + 1.0e-5) + +# swarm = uw.swarm.Swarm(mesh=discmesh) +v_soln = uw.discretisation.MeshVariable("U", discmesh, discmesh.dim, degree=2) +t_soln = uw.discretisation.MeshVariable(r"\Delta T", discmesh, 1, degree=2) +flux = uw.discretisation.MeshVariable(r"dTdz", discmesh, 1, degree=2) + +# + +# v_soln.read_timestep(checkpoint_base, "U", step, outputPath=checkpoint_dir) +# t_soln.read_timestep(checkpoint_base, "T", step, outputPath=checkpoint_dir) +# - + + +steps = range(200,6650,10) + +# + +import mpi4py +import pyvista as pv +import underworld3.visualisation as vis + +pl = pv.Plotter(window_size=[1000, 1000]) + +for step in steps: + + try: + v_soln.read_timestep(checkpoint_base, "U", step, outputPath=checkpoint_dir) + t_soln.read_timestep(checkpoint_base, "T", step, outputPath=checkpoint_dir) + except: + continue + + pl.clear() + + pvmesh = vis.mesh_to_pv_mesh(discmesh) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym[0]) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym) + + # point sources at cell centres + skip = 3 + points = np.zeros((discmesh._centroids[::skip].shape[0], 3)) + points[:, 0] = discmesh._centroids[::skip, 0] + points[:, 1] = discmesh._centroids[::skip, 1] + point_cloud = pv.PolyData(points) + + pvstream = pvmesh.streamlines_from_source( + point_cloud, + vectors="V", + integration_direction="both", + max_time=1, + surface_streamlines=True, + ) + + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Grey", + show_edges=False, + scalars="T", + use_transparency=False, + opacity=1.0, + ) + + pl.add_mesh(pvstream, opacity=0.4, show_scalar_bar=False) + pl.add_mesh(pvmesh, "Black", "wireframe", opacity=0.1) + + pl.add_points(point_cloud, color="White", point_size=3.0, opacity=0.25) + + # pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.001, show_scalar_bar=True) + + + # pl.remove_scalar_bar("V") + + imagefile = os.path.join(checkpoint_dir, checkpoint_base) + f"{step}.png" + + pl.screenshot(filename=imagefile, window_size=(1000, 1000), return_img=False) + + + +# - +pl.show() + +uw.systems.Stokes.view() + + + +# + +## Calculate heat flux, evaluate at surface — proxy for boundary layer thickness + +# + +flux_solver = uw.systems.Projection(discmesh, flux) + +# Conductive flux only ! +radial_flux = -discmesh.vector.gradient(t_soln.sym[0]).dot(discmesh.CoordinateSystem.unit_e_0) +radial_flux *= sympy.exp(-100*(r-1)**2) + +flux_solver.uw_function = radial_flux +flux_solver.smoothing = 1.0e-3 +flux_solver.solve() + +# + +import mpi4py +import pyvista as pv +import underworld3.visualisation as vis + +pvmesh = vis.mesh_to_pv_mesh(discmesh) +pvmesh.point_data["dTdz"] = vis.scalar_fn_to_pv_points(pvmesh, flux.sym[0]) +pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) +pvmesh.point_data["V"] -= pvmesh.point_data["V"].mean() + + +velocity_points = vis.meshVariable_to_pv_cloud(v_soln) +velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym) + +# point sources at cell centres +skip = 1 +points = np.zeros((discmesh._centroids[::skip].shape[0], 3)) +points[:, 0] = discmesh._centroids[::skip, 0] +points[:, 1] = discmesh._centroids[::skip, 1] +point_cloud = pv.PolyData(points) + +pvstream = pvmesh.streamlines_from_source( + point_cloud, + vectors="V", + integration_direction="both", + max_time=0.5, +) + +pl = pv.Plotter(window_size=[1000, 1000]) + +pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Grey", + show_edges=True, + scalars="dTdz", + use_transparency=False, + opacity=1.0, +) + +# pl.add_mesh(pvstream, opacity=0.4, show_scalar_bar=False) +# pl.add_mesh(pvmesh, "Black", "wireframe", opacity=0.1) + +pl.add_points(point_cloud, color="White", point_size=3.0, opacity=0.25) + +pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.002, show_scalar_bar=True) + + +# pl.remove_scalar_bar("V") + +imagefile = os.path.join(checkpoint_dir, checkpoint_base) + f"{step}.png" + +pl.screenshot(filename=imagefile, window_size=(1000, 1000), return_img=False) +# OR + +# - + + diff --git a/main/_sources/Notebooks/Examples-PostProcessing/Ex_Navier_Stokes_Visualise_Disc.py b/main/_sources/Notebooks/Examples-PostProcessing/Ex_Navier_Stokes_Visualise_Disc.py new file mode 100644 index 0000000..c072711 --- /dev/null +++ b/main/_sources/Notebooks/Examples-PostProcessing/Ex_Navier_Stokes_Visualise_Disc.py @@ -0,0 +1,264 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + + +# # Navier Stokes test: flow in an annulus with a moving boundary (2D) +# + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +import os +import petsc4py +import underworld3 as uw +import numpy as np +import sympy + +# + language="sh" +# +# ls -tr /Users/lmoresi/+Simulations/NS_benchmarks/ +# #ls -tr /Users/lmoresi/+Underworld/underworld3/JupyterBook/Notebooks/Examples-NavierStokes/output_res_033/*mesh*h5 | tail -10 +# +# - + + +# ls -tr /Users/lmoresi/+Simulations/NS_benchmarks/NS_Annulus_Re1000/Cylinder_NS_rho_1000_25*omega* | tail -5 + +# + +## Reading the checkpoints back in ... + +step = 320 + +checkpoint_dir = "/Users/lmoresi/+Simulations/NS_benchmarks/NS_Annulus_Re1000" +# checkpoint_dir = "/Users/lmoresi/+Underworld/underworld3/JupyterBook/Notebooks/Examples-NavierStokes//Users/lmoresi/+Simulations/NS_benchmarks/NS_BMK_DvDt_std" + +checkpoint_base = "Cylinder_NS_rho_1000_25" +base_filename = os.path.join(checkpoint_dir, checkpoint_base) + +# + +# mesh = uw.discretisation.Mesh(f"{base_filename}.mesh.{step:05d}.h5") +mesh = uw.discretisation.Mesh(f"{base_filename}.mesh.00000.h5") + +v_soln_ckpt = uw.discretisation.MeshVariable("U", mesh, mesh.dim, degree=2) +p_soln_ckpt = uw.discretisation.MeshVariable("P", mesh, 1, degree=1) +vorticity_ckpt = uw.discretisation.MeshVariable("omega", mesh, 1, degree=1) + +passive_swarm_ckpt = uw.swarm.Swarm(mesh) +active_swarm_ckpt = uw.swarm.Swarm(mesh) +# - + + + + +# + +v_soln_ckpt.read_timestep(checkpoint_base, "U", step, outputPath=checkpoint_dir) +p_soln_ckpt.read_timestep(checkpoint_base, "P", step, outputPath=checkpoint_dir) +vorticity_ckpt.read_timestep(checkpoint_base, "omega", step, outputPath=checkpoint_dir) + +# This one is just the individual points +passive_swarm_ckpt.read_timestep(checkpoint_base, "passive_swarm", step, outputPath=checkpoint_dir) + + +# + +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh) + pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln_ckpt.sym[0]) + pvmesh.point_data["Omega"] = vis.scalar_fn_to_pv_points(pvmesh, vorticity_ckpt.sym) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln_ckpt.sym) + pvmesh.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh, sympy.sqrt(v_soln_ckpt.sym.dot(v_soln_ckpt.sym))) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln_ckpt) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln_ckpt.sym) + + x,y = mesh.X + U0 = 1.5 + Vb = (4.0 * U0 * y * (0.41 - y)) / 0.41**2 + + # swarm points + points = vis.swarm_to_pv_cloud(passive_swarm_ckpt) + swarm_point_cloud = pv.PolyData(points) + + # point sources at cell centres + skip = 5 + points = np.zeros((mesh._centroids[::skip].shape[0], 3)) + points[:, 0] = mesh._centroids[::skip, 0] + points[:, 1] = mesh._centroids[::skip, 1] + point_cloud = pv.PolyData(points) + + pvstream = pvmesh.streamlines_from_source( + point_cloud, + vectors="V", + integration_direction="both", + max_time=0.5, + ) + + pl = pv.Plotter(window_size=(1000, 750)) + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.03, opacity=0.5) + + pl.add_mesh( + pvmesh, + cmap="bwr", + edge_color="Black", + show_edges=False, + scalars="Omega", + clim=[-1000,1000], + use_transparency=False, + opacity=1.0, + ) + + pl.add_points(swarm_point_cloud, color="Black", + render_points_as_spheres=True, + point_size=3, opacity=0.25 + ) + + pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.25) + pl.add_mesh(pvstream, opacity=0.5) + + pl.remove_scalar_bar("Omega") + pl.remove_scalar_bar("mag") + pl.remove_scalar_bar("V") + + pl.camera.SetPosition(0.0, 0.0, 4.7) + pl.camera.SetFocalPoint(0.0, 0.0, 0.0) + pl.camera.SetClippingRange(1.0, 8.0) + + + pl.screenshot( + filename=f"{base_filename}.{step}.png", + window_size=(1600, 1600), + return_img=False, + ) + + pl.show(jupyter_backend="client") +# - + + +0/0 + +# + +import glob +steps = [] +U_files = glob.glob(f"{checkpoint_dir}/{checkpoint_base}.mesh.U*h5") +for Uf in U_files: + steps.append(int(Uf.split('.U.')[1].split('.')[0])) +steps.sort() + +print(steps) + + +# + +# Override output range (but need to run above cell to get the files themselves) + +# steps = range(0,100,5) + +# + +if uw.mpi.size == 1: + import pyvista as pv + import underworld3.visualisation as vis + + pl = pv.Plotter(window_size=(1000, 750)) + +for step in steps: + # check the mesh if in a notebook / serial + + v_soln_ckpt.read_timestep(checkpoint_base, "U", step, outputPath=checkpoint_dir, verbose=True) + p_soln_ckpt.read_timestep(checkpoint_base, "P", step, outputPath=checkpoint_dir, verbose=True) + vorticity_ckpt.read_timestep(checkpoint_base, "omega", step, outputPath=checkpoint_dir, verbose=True) + +# This one is just the individual points + passive_swarm_ckpt = uw.swarm.Swarm(mesh) + passive_swarm_ckpt.read_timestep(checkpoint_base, "passive_swarm", step, outputPath=checkpoint_dir) + + pvmesh = vis.mesh_to_pv_mesh(mesh) + pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln_ckpt.sym) + pvmesh.point_data["Omega"] = vis.scalar_fn_to_pv_points(pvmesh, vorticity_ckpt.sym) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln_ckpt.sym) + pvmesh.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh, sympy.sqrt(v_soln_ckpt.sym.dot(v_soln_ckpt.sym))) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln_ckpt) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln_ckpt.sym) + + x,y = mesh.X + U0 = 1.5 + Vb = (4.0 * U0 * y * (0.41 - y)) / 0.41**2 + + # swarm points + points = vis.swarm_to_pv_cloud(passive_swarm_ckpt) + swarm_point_cloud = pv.PolyData(points) + + # point sources at cell centres + skip = 15 + points = np.zeros((mesh._centroids[::skip].shape[0], 3)) + points[:, 0] = mesh._centroids[::skip, 0] + points[:, 1] = mesh._centroids[::skip, 1] + point_cloud = pv.PolyData(points) + + pvstream = pvmesh.streamlines_from_source( + point_cloud, + vectors="V", + integration_direction="both", + max_time=0.5, + ) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.03, opacity=0.5) + + + pl.add_points(swarm_point_cloud, color="Black", + render_points_as_spheres=True, + point_size=3, opacity=0.5 + ) + + pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.05) + + pl.add_mesh( + pvmesh, + cmap="bwr", + edge_color="Black", + show_edges=False, + scalars="Omega", + clim=[-250,250], + use_transparency=False, + opacity=0.75, + ) + + pl.add_mesh(pvstream, opacity=0.5) + + + pl.remove_scalar_bar("Omega") + # pl.remove_scalar_bar("mag") + pl.remove_scalar_bar("V") + + pl.camera.SetPosition(0.0, 0.0, 4.7) + pl.camera.SetFocalPoint(0.0, 0.0, 0.0) + pl.camera.SetClippingRange(1.0, 8.0) + + pl.screenshot( + filename=f"{base_filename}.{step}.png", + window_size=(1600, 1600), + return_img=False, + ) + + pl.clear() +# - +# ! open . + + + diff --git a/main/_sources/Notebooks/Examples-PostProcessing/Ex_Navier_Stokes_Visualise_NS_DFG_2d.py b/main/_sources/Notebooks/Examples-PostProcessing/Ex_Navier_Stokes_Visualise_NS_DFG_2d.py new file mode 100644 index 0000000..25ebb22 --- /dev/null +++ b/main/_sources/Notebooks/Examples-PostProcessing/Ex_Navier_Stokes_Visualise_NS_DFG_2d.py @@ -0,0 +1,275 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + + +# # Navier Stokes test: flow around a circular inclusion (2D) +# +# http://www.mathematik.tu-dortmund.de/~featflow/en/benchmarks/cfdbenchmarking/flow/dfg_benchmark1_re20.html +# +# No slip conditions +# +# ![](http://www.mathematik.tu-dortmund.de/~featflow/media/dfg_bench1_2d/geometry.png) +# +# Note ... +# +# In this benchmark, I have scaled $\rho = 1000$ and $\nu = 1.0$ as otherwise it fails to converge. This occurs because we are locked into a range of $\Delta t$ by the flow velocity (and accurate particle transport), and by the assumption that $\dot{\epsilon}$ is computed in the Eulerian form. The Crank-Nicholson scheme still has some timestep requirements associated with diffusivity (viscosity in this case) and this may be what I am seeing. +# +# Velocity is the same, but pressure scales by 1000. This should encourage us to implement scaling / units. +# +# Model 4 is not one of the benchmarks, but just turns up the Re parameter to see if the mesh can resolve higher values than 100 +# +# + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +import os +import petsc4py +import underworld3 as uw +import numpy as np +import sympy + +# + language="sh" +# +# ls -tr /Users/lmoresi/+Simulations/NS_benchmarks/NS_BMK_Re1000 +# #ls -tr /Users/lmoresi/+Underworld/underworld3/JupyterBook/Notebooks/Examples-NavierStokes/output_res_033/*mesh*h5 | tail -10 +# +# - + + +# ls -tr /Users/lmoresi/+Simulations/NS_benchmarks/NS_BMK_Re1000/output_res_60_3.0/NS_test_Re_1000_SLCN_60*mesh* | tail + +# + +## Reading the checkpoints back in ... + +step = 190 + +checkpoint_dir = "/Users/lmoresi/+Simulations/NS_benchmarks/NS_BMK_Re1000/output_res_60_3.0" +# checkpoint_dir = "/Users/lmoresi/+Underworld/underworld3/JupyterBook/Notebooks/Examples-NavierStokes//Users/lmoresi/+Simulations/NS_benchmarks/NS_BMK_DvDt_std" + +checkpoint_base = "NS_test_Re_1000_SLCN_60" +base_filename = os.path.join(checkpoint_dir, checkpoint_base) + +# + +# mesh = uw.discretisation.Mesh(f"{base_filename}.mesh.{step:05d}.h5") +mesh = uw.discretisation.Mesh(f"{base_filename}.mesh.00000.h5") + +v_soln_ckpt = uw.discretisation.MeshVariable("U", mesh, mesh.dim, degree=2) +p_soln_ckpt = uw.discretisation.MeshVariable("P", mesh, 1, degree=1) +vorticity_ckpt = uw.discretisation.MeshVariable("omega", mesh, 1, degree=1) + +passive_swarm_ckpt = uw.swarm.Swarm(mesh) +active_swarm_ckpt = uw.swarm.Swarm(mesh) + + +# + +v_soln_ckpt.read_timestep(checkpoint_base, "U", step, outputPath=checkpoint_dir) +p_soln_ckpt.read_timestep(checkpoint_base, "P", step, outputPath=checkpoint_dir) +vorticity_ckpt.read_timestep(checkpoint_base, "omega", step, outputPath=checkpoint_dir) + +# This one is just the individual points +passive_swarm_ckpt.read_timestep(checkpoint_base, "passive_swarm", step, outputPath=checkpoint_dir) + + +# + +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh) + pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln_ckpt.sym[0]) + pvmesh.point_data["Omega"] = vis.scalar_fn_to_pv_points(pvmesh, vorticity_ckpt.sym) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln_ckpt.sym) + pvmesh.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh, sympy.sqrt(v_soln_ckpt.sym.dot(v_soln_ckpt.sym))) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln_ckpt) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln_ckpt.sym) + + x,y = mesh.X + U0 = 1.5 + Vb = (4.0 * U0 * y * (0.41 - y)) / 0.41**2 + + # swarm points + points = vis.swarm_to_pv_cloud(passive_swarm_ckpt) + swarm_point_cloud = pv.PolyData(points) + + # point sources at cell centres + skip = 5 + points = np.zeros((mesh._centroids[::skip].shape[0], 3)) + points[:, 0] = mesh._centroids[::skip, 0] + points[:, 1] = mesh._centroids[::skip, 1] + point_cloud = pv.PolyData(points) + + pvstream = pvmesh.streamlines_from_source( + point_cloud, + vectors="V", + integration_direction="both", + max_time=0.5, + ) + + pl = pv.Plotter(window_size=(1000, 750)) + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.001, opacity=0.5) + + pl.add_mesh( + pvmesh, + cmap="bwr", + edge_color="Black", + show_edges=False, + scalars="Omega", + use_transparency=False, + opacity=1.0, + ) + + pl.add_points(swarm_point_cloud, color="Black", + render_points_as_spheres=True, + point_size=3, opacity=0.2 + ) + + pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.25) + pl.add_mesh(pvstream, opacity=0.5) + + + pl.remove_scalar_bar("Omega") + pl.remove_scalar_bar("mag") + pl.remove_scalar_bar("V") + + + pl.camera.SetPosition(1.15, 0.2, 1.7) + pl.camera.SetFocalPoint(1.15, 0.2, 0.0) + pl.camera.SetClippingRange(1.0, 8.0) + + + pl.screenshot( + filename=f"{base_filename}.{step}.png", + window_size=(2560, 1280), + return_img=False, + ) + + pl.show(jupyter_backend="client") +# - + + +0/0 + +# + +import glob +steps = [] +U_files = glob.glob(f"{checkpoint_dir}/{checkpoint_base}.mesh.U*h5") +for Uf in U_files: + steps.append(int(Uf.split('.U.')[1].split('.')[0])) +steps.sort() + +print(steps) + + +# + +# Override output range (but need to run above cell to get the files themselves) + +# steps = range(0,360,10) + +# + +if uw.mpi.size == 1: + import pyvista as pv + import underworld3.visualisation as vis + + pl = pv.Plotter(window_size=(1000, 750)) + +for step in steps: + # check the mesh if in a notebook / serial + + v_soln_ckpt.read_timestep(checkpoint_base, "U", step, outputPath=checkpoint_dir, verbose=True) + p_soln_ckpt.read_timestep(checkpoint_base, "P", step, outputPath=checkpoint_dir, verbose=True) + vorticity_ckpt.read_timestep(checkpoint_base, "omega", step, outputPath=checkpoint_dir, verbose=True) + +# This one is just the individual points + passive_swarm_ckpt = uw.swarm.Swarm(mesh) + passive_swarm_ckpt.read_timestep(checkpoint_base, "passive_swarm", step, outputPath=checkpoint_dir) + + pvmesh = vis.mesh_to_pv_mesh(mesh) + pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln_ckpt.sym) + pvmesh.point_data["Omega"] = vis.scalar_fn_to_pv_points(pvmesh, vorticity_ckpt.sym) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln_ckpt.sym) + pvmesh.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh, sympy.sqrt(v_soln_ckpt.sym.dot(v_soln_ckpt.sym))) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln_ckpt) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln_ckpt.sym) + + x,y = mesh.X + U0 = 1.5 + Vb = (4.0 * U0 * y * (0.41 - y)) / 0.41**2 + + # swarm points + points = vis.swarm_to_pv_cloud(passive_swarm_ckpt) + swarm_point_cloud = pv.PolyData(points) + + # point sources at cell centres + skip = 15 + points = np.zeros((mesh._centroids[::skip].shape[0], 3)) + points[:, 0] = mesh._centroids[::skip, 0] + points[:, 1] = mesh._centroids[::skip, 1] + point_cloud = pv.PolyData(points) + + pvstream = pvmesh.streamlines_from_source( + point_cloud, + vectors="V", + integration_direction="both", + max_time=0.5, + ) + + # pl.add_arrows(arrow_loc, arrow_length, mag=0.02, opacity=0.5) + + + + pl.add_points(swarm_point_cloud, color="Black", + render_points_as_spheres=True, + point_size=3, opacity=0.5 + ) + + pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.05) + + pl.add_mesh( + pvmesh, + cmap="bwr", + edge_color="Black", + show_edges=False, + scalars="Omega", + clim=[-1000,1000], + use_transparency=False, + opacity=0.75, + ) + + pl.add_mesh(pvstream, opacity=0.5) + + + pl.remove_scalar_bar("Omega") + # pl.remove_scalar_bar("mag") + pl.remove_scalar_bar("V") + + pl.camera.SetPosition(0.95, 0.2, 1.5) + pl.camera.SetFocalPoint(0.95, 0.2, 0.0) + pl.camera.SetClippingRange(1.0, 8.0) + + pl.screenshot( + filename=f"{base_filename}.{step}.png", + window_size=(2500, 1000), + return_img=False, + ) + + pl.clear() +# - + diff --git a/main/_sources/Notebooks/Examples-PostProcessing/Ex_SphericalStokes_Visualise.py b/main/_sources/Notebooks/Examples-PostProcessing/Ex_SphericalStokes_Visualise.py new file mode 100644 index 0000000..d732c28 --- /dev/null +++ b/main/_sources/Notebooks/Examples-PostProcessing/Ex_SphericalStokes_Visualise.py @@ -0,0 +1,153 @@ +# ## Visualise spherical stokes model (velocity, particles etc) + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +import petsc4py +import underworld3 as uw +import numpy as np + +# ls -tr /Users/lmoresi/+Simulations/InnerCore/outputs_free_slip_fk1e-2_ViscGrad0_iic100_QTemp_mr + +# + +checkpoint_dir = "/Users/lmoresi/+Simulations/InnerCore/outputs_free_slip_fk1e-2_ViscGrad0_iic100_QTemp_mr" +checkpoint_base = f"free_slip_sphere" +# basename = f"/Users/lmoresi/+Simulations/InnerCore/outputs_free_slip_fk1e-2_ViscGrad0_iic0_QTemp_mr/free_slip_sphere.h5" + +step = 210 + +res = uw.options.getReal("resolution", default=0.1) +r_o = uw.options.getReal("radius_o", default=1.0) +r_i = uw.options.getReal("radius_i", default=0.05) + + + +# + +# # ls -ltr ~/+Simulations/InnerCore/outputs_free_slip_fk1e-2_ViscGrad0_iic100_QTemp_mr | tail + +# + +meshball = uw.meshing.SphericalShell( + radiusInner=r_i, + radiusOuter=r_o, + cellSize=res, + qdegree=2, +) + +swarm = uw.swarm.Swarm(mesh=meshball) +v_soln = uw.discretisation.MeshVariable("U", meshball, meshball.dim, degree=2) +t_soln = uw.discretisation.MeshVariable(r"\Delta T", meshball, 1, degree=2) + +# - + +print(f"Read swarm data", flush=True) +swarm.load(f"{basename}.passive_swarm.{step}.h5") + +v_soln.read_timestep(checkpoint_base, "u", 0, outputPath=checkpoint_dir) +t_soln.read_timestep(checkpoint_base, "deltaT", 0, outputPath=checkpoint_dir) + + +# + +# v_soln.read_timestep +# t_soln.read_from_vertex_checkpoint(f"{basename}.DeltaT.0.h5", "DeltaT") + +# + +import mpi4py + +if mpi4py.MPI.COMM_WORLD.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym) + + # point sources at cell centres + skip = 250 + points = np.zeros((meshball._centroids[::skip].shape[0], 3)) + points[:, 0] = meshball._centroids[::skip, 0] + points[:, 1] = meshball._centroids[::skip, 1] + points[:, 2] = meshball._centroids[::skip, 1] + point_cloud = pv.PolyData(points) + + pvstream = pvmesh.streamlines_from_source( + point_cloud, + vectors="V", + integration_direction="both", + # max_time=2.0, + ) + + with swarm.access(): + points = swarm.data.copy() + r2 = points[:,0]**2 + points[:,1]**2 + points[:,2]**2 + point_cloud = pv.PolyData(points[r2<0.98**2]) + # point_cloud.point_data["strain"] = strain.data[:,0] + + sphere = pv.Sphere(radius=0.85, center=(0.0, 0.0, 0.0)) + clipped = pvmesh.clip_surface(sphere) + + # clipped = pvmesh.clip(origin=(0.0, 0.0, 0.0), normal=(0.1, 0, 1), invert=True) + + pl = pv.Plotter(window_size=[1000, 1000]) + # pl.add_axes() + + pl.camera_position = [(2.1,-4.0,0.0), (0.0,0.0,0.0), (0.0,0.0,1.0)] + # pl.camera. + # pl.camera.azimuth = -65 + # pl.camera.distance = 10.0 + + # pl.camera_position = [(0.00036144256591796875, -0.00045242905616760254, 6.692800318757354), + # (0.00036144256591796875, -0.00045242905616760254, 0.00010478496551513672), + # (0.0, 1.0, 0.0)] + + pl.add_mesh( + clipped, + cmap="coolwarm", + edge_color="Black", + show_edges=False, + scalars="T", + use_transparency=False, + opacity=1.0, + ) + + pl.add_mesh(pvstream, opacity=0.4) + pl.add_mesh(pvmesh, "Black", "wireframe", opacity=0.1) + + # pl.add_mesh(pvmesh, cmap="coolwarm", edge_color="Black", show_edges=True, scalars="T", + # use_transparency=False, opacity=1.0) + + pl.add_points(point_cloud, color="White", point_size=3.0, opacity=0.25) + + # pl.add_arrows(arrow_loc, arrow_length, mag=20) + + pl.remove_scalar_bar("T") + try: + pl.remove_scalar_bar("mag") + except KeyError: + pass + try: + pl.remove_scalar_bar("V-normed") + except KeyError: + pass + try: + pl.remove_scalar_bar("V") + except KeyError: + pass + + + # pl.remove_scalar_bar("V") + + pl.screenshot(filename="sphere_iic0.png", window_size=(1000, 1000), return_img=False) + # OR + pl.show() + +# + language="sh" +# +# open sphere_iic0.png +# - + + diff --git a/main/_sources/Notebooks/Examples-Sandbox-VEP/Ex_Compression_Example.py b/main/_sources/Notebooks/Examples-Sandbox-VEP/Ex_Compression_Example.py new file mode 100644 index 0000000..f648d07 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Sandbox-VEP/Ex_Compression_Example.py @@ -0,0 +1,392 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Compression / Extension with no mesh deformation +# +# This is a rigid inclusion model so it looks a lot like Ex_Shear_Band_Plasticity_PS.py but the geometry is closer to +# what we have seen before in various papers. +# +# The yield stress is Drucker-Prager / Von Mises ($\mu$ = 0). +# +# ## Examples: +# +# Try $C = 0.1$ and $\mu = 0$ to see highly developed shear bands +# +# Try $C = 0.05$ and $\mu = 0.5$ which does not localise as strongly but is highly non-linear nonetheless. +# + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + + +C0 = 0.0001 +mu0 = 0.3 + +expt_name = f"Compression_C{C0}_mu{mu0}" +# - + +import petsc4py +import underworld3 as uw +import numpy as np + + +options = petsc4py.PETSc.Options() +options["dm_adaptor"] = "pragmatic" + + +# + +import gmsh + +# Mesh a 2D pipe with a circular hole + +csize = 0.33 # 0.033 +csize_inclusion = 0.02 +res = csize_inclusion + +width = 2.0 +height = 1.0 +radius = 0.25 + +if uw.mpi.rank == 0: + # Generate local mesh on boss process + + gmsh.initialize() + gmsh.model.add("Notch") + gmsh.model.geo.characteristic_length_max = csize + + c0 = gmsh.model.geo.add_point(0.0, 0.0, 0.0, csize_inclusion) + cr1 = gmsh.model.geo.add_point(-radius, 0.0, 0.0, csize_inclusion) + cr2 = gmsh.model.geo.add_point(0.0, radius, 0.0, csize_inclusion) + cr3 = gmsh.model.geo.add_point(+radius, 0.0, 0.0, csize_inclusion) + cr4 = gmsh.model.geo.add_point(-radius, radius, 0.0, csize_inclusion) + cr5 = gmsh.model.geo.add_point(+radius, radius, 0.0, csize_inclusion) + + cp1 = gmsh.model.geo.add_point(-width, 0.0, 0.0, csize) + cp2 = gmsh.model.geo.add_point(+width, 0.0, 0.0, csize) + cp3 = gmsh.model.geo.add_point(+width, height, 0.0, csize) + cp4 = gmsh.model.geo.add_point(-width, height, 0.0, csize) + + l1 = gmsh.model.geo.add_line(cr3, cp2) + l2 = gmsh.model.geo.add_line(cp2, cp3) + l3 = gmsh.model.geo.add_line(cp3, cp4) + l4 = gmsh.model.geo.add_line(cp4, cp1) + l5 = gmsh.model.geo.add_line(cp1, cr1) + + l6 = gmsh.model.geo.add_circle_arc(cr1, c0, cr2) + l7 = gmsh.model.geo.add_circle_arc(cr2, c0, cr3) + # l6 = gmsh.model.geo.add_line(cr1, cr4) + # l7 = gmsh.model.geo.add_line(cr4, cr5) + # l8 = gmsh.model.geo.add_line(cr5, cr3) + + cl1 = gmsh.model.geo.add_curve_loop([l1, l2, l3, l4, l5, l6, l7]) + surf1 = gmsh.model.geo.add_plane_surface( + [cl1], + ) + + gmsh.model.geo.synchronize() + + gmsh.model.add_physical_group(1, [l4], -1, name="Left") + gmsh.model.add_physical_group(1, [l2], -1, name="Right") + gmsh.model.add_physical_group(1, [l3], -1, name="Top") + gmsh.model.add_physical_group(1, [l1, l5], -1, name="FlatBottom") + gmsh.model.add_physical_group(1, [l6, l7], -1, name="Hump") + gmsh.model.add_physical_group(2, [surf1], -1, name="Elements") + + gmsh.model.mesh.generate(2) + + gmsh.write(f"tmp_hump.msh") + gmsh.finalize() +# - + + +mesh1 = uw.discretisation.Mesh("tmp_hump.msh", useRegions=True, simplex=True) +mesh1.dm.view() + +# + +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh("tmp_hump.msh") + + pl = pv.Plotter(window_size=(1000, 750)) + + points = np.zeros((mesh1._centroids.shape[0], 3)) + points[:, 0] = mesh1._centroids[:, 0] + points[:, 1] = mesh1._centroids[:, 1] + + point_cloud = pv.PolyData(points) + + # pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.5) + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + use_transparency=False, + opacity=0.5, + ) + + # + + pl.show(cpos="xy") + +# + +# Define some functions on the mesh + +import sympy + +# radius_fn = sympy.sqrt(mesh1.rvec.dot(mesh1.rvec)) # normalise by outer radius if not 1.0 +# unit_rvec = mesh1.rvec / (1.0e-10+radius_fn) + +# Some useful coordinate stuff + +x, y = mesh1.X + +# relative to the centre of the inclusion +r = sympy.sqrt(x**2 + y**2) +th = sympy.atan2(y, x) + +# need a unit_r_vec equivalent + +inclusion_rvec = mesh1.X +inclusion_unit_rvec = inclusion_rvec / inclusion_rvec.dot(inclusion_rvec) +inclusion_unit_rvec = mesh1.vector.to_matrix(inclusion_unit_rvec) + +# Pure shear flow + +vx_ps = mesh1.N.x +vy_ps = -mesh1.N.y +# + +v_soln = uw.discretisation.MeshVariable("U", mesh1, mesh1.dim, degree=2) +t_soln = uw.discretisation.MeshVariable("T", mesh1, 1, degree=2) +p_soln = uw.discretisation.MeshVariable("P", mesh1, 1, degree=1) + +vorticity = uw.discretisation.MeshVariable("omega", mesh1, 1, degree=1) +strain_rate_inv2 = uw.discretisation.MeshVariable("eps", mesh1, 1, degree=1) +dev_stress_inv2 = uw.discretisation.MeshVariable("tau", mesh1, 1, degree=1) +node_viscosity = uw.discretisation.MeshVariable("eta", mesh1, 1, degree=1) +r_inc = uw.discretisation.MeshVariable("R", mesh1, 1, degree=1) + + +# + +# Create Stokes solver object + +stokes = uw.systems.Stokes( + mesh1, + velocityField=v_soln, + pressureField=p_soln, + verbose=False, + solver_name="stokes", +) + +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.viscosity = 1 +stokes.saddle_preconditioner = 1 / stokes.constitutive_model.Parameters.viscosity +stokes.penalty = 0.1 + +stokes.petsc_options["ksp_monitor"] = None +stokes.petsc_options["snes_atol"] = 1.0e-4 + + +stokes.petsc_options["fieldsplit_velocity_ksp_type"] = "cg" +stokes.petsc_options["fieldsplit_velocity_pc_type"] = "mg" + +stokes.petsc_options["fieldsplit_pressure_ksp_type"] = "gmres" +stokes.petsc_options["fieldsplit_pressure_pc_type"] = "mg" + + +# + +nodal_strain_rate_inv2 = uw.systems.Projection( + mesh1, strain_rate_inv2, solver_name="edot_II" +) +nodal_strain_rate_inv2.add_dirichlet_bc(1.0, "Left", 0) +nodal_strain_rate_inv2.add_dirichlet_bc(1.0, "Right", 0) +nodal_strain_rate_inv2.uw_function = stokes.Unknowns.Einv2 +nodal_strain_rate_inv2.smoothing = 0.0e-3 +nodal_strain_rate_inv2.petsc_options.delValue("ksp_monitor") + +nodal_tau_inv2 = uw.systems.Projection(mesh1, dev_stress_inv2, solver_name="stress_II") + +S = stokes.stress_deviator +nodal_tau_inv2.uw_function = ( + sympy.simplify(sympy.sqrt(((S**2).trace()) / 2)) - p_soln.sym[0] +) +nodal_tau_inv2.smoothing = 0.0e-3 +nodal_tau_inv2.petsc_options.delValue("ksp_monitor") + +nodal_visc_calc = uw.systems.Projection(mesh1, node_viscosity, solver_name="visc") +nodal_visc_calc.uw_function = stokes.constitutive_model.Parameters.viscosity +nodal_visc_calc.smoothing = 1.0e-3 +nodal_visc_calc.petsc_options.delValue("ksp_monitor") + + +# + +# Set solve options here (or remove default values +# stokes.petsc_options.getAll() + +# Constant visc + +stokes.bodyforce = -1 * mesh1.CoordinateSystem.unit_j + +hw = 1000.0 / res +hump_surface_fn = sympy.exp(-(((r - radius) / radius) ** 2) * hw) +upper_surface_fn = sympy.exp(-(((y - height)) ** 2) * hw) + +stokes.bodyforce -= ( + 1.0e6 * hump_surface_fn * v_soln.sym.dot(inclusion_unit_rvec) * inclusion_unit_rvec +) + +# stokes.bodyforce +p_penalty = 0.0 +stokes.PF0 = p_penalty * upper_surface_fn * p_soln.sym +stokes.saddle_preconditioner = ( + 1 / stokes.constitutive_model.Parameters.viscosity + p_penalty * upper_surface_fn +) + +# Velocity boundary conditions + +# stokes.add_dirichlet_bc((0.0, 0.0), "Hump", (0, 1)) +# stokes.add_dirichlet_bc((vx_ps, vy_ps), ["top", "bottom", "left", "right"], (0, 1)) +stokes.add_dirichlet_bc((1.0, 0.0), "Left", (0, 1)) +stokes.add_dirichlet_bc((-1.0, 0.0), "Right", (0, 1)) +stokes.add_dirichlet_bc((0.0,), "FlatBottom", (1,)) + + +# + +# linear solve first + +stokes.solve(zero_init_guess=False) +# + +# Calculate surface pressure + +_, _, _, _, ps_sum, _, _ = mesh1.stats(p_soln.sym[0] * upper_surface_fn, p_soln) +_, _, _, _, p_sum, _, _ = mesh1.stats(p_soln.sym[0], p_soln) +_, _, _, _, ps_norm, _, _ = mesh1.stats(upper_surface_fn, p_soln) +_, _, _, _, p_norm, _, _ = mesh1.stats(1 + 0.00001 * p_soln.sym[0], p_soln) + +print(f"Mean Surface P - {ps_sum/p_sum}") +print(f"Mean P - {p_sum/p_norm}") + + +# p_calculator = uw.maths.Integral(mesh1, p_soln.sym[0] * upper_surface_fn) +# value = p_calculator.evaluate() + +# # calculator.fn = upper_surface_fn +# # norm = calculator.evaluate() + +# integral = value # / norm + +# print(f"Average surface pressure: {integral}") + +# + p_penalty * upper_surface_fn) + +# stokes.solve(zero_init_guess=False) + + +# + +# Approach the required value by shifting the parameters + +for i in range(1): + mu = mu0 + C = C0 # + (1 - i / 4) * 0.1 + print(f"Mu - {mu}, C = {C}") + tau_y = sympy.Max( + C + mu * stokes.p.sym[0] + 1 * sympy.sin(x * sympy.pi / (2 * width)) ** 2, + 0.0001, + ) + viscosity = 1.0 / (2 * stokes.Unknowns.Einv2 / tau_y + 1.0) + + stokes.constitutive_model.Parameters.viscosity = viscosity + stokes.saddle_preconditioner = ( + 1 / stokes.constitutive_model.Parameters.viscosity + + p_penalty * upper_surface_fn + ) + stokes.solve(zero_init_guess=False) +# - + +nodal_tau_inv2.uw_function = ( + stokes.constitutive_model.Parameters.viscosity * stokes.Unknowns.Einv2 +) +nodal_tau_inv2.solve() +nodal_visc_calc.uw_function = stokes.constitutive_model.Parameters.viscosity +nodal_visc_calc.solve() +nodal_strain_rate_inv2.solve() + + +mesh1.petsc_save_checkpoint(index=0, meshVars=[v_soln, p_soln, dev_stress_inv2, strain_rate_inv2, node_viscosity], + outputPath="./output/") + + +# + +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh1) + pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym) + pvmesh.point_data["Edot"] = vis.scalar_fn_to_pv_points(pvmesh, strain_rate_inv2.sym) + pvmesh.point_data["Visc"] = vis.scalar_fn_to_pv_points(pvmesh, node_viscosity.sym) + pvmesh.point_data["Str"] = vis.scalar_fn_to_pv_points(pvmesh, dev_stress_inv2.sym) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) + pvmesh.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh, v_soln.sym.dot(v_soln.sym)) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym) + + + # point sources at cell centres + points = np.zeros((mesh1._centroids.shape[0], 3)) + points[:, 0] = mesh1._centroids[:, 0] + points[:, 1] = mesh1._centroids[:, 1] + point_cloud = pv.PolyData(points) + + pvstream = pvmesh.streamlines_from_source( + point_cloud, vectors="V", integration_direction="both", max_steps=100 + ) + + pl = pv.Plotter(window_size=(1000, 750)) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.05, opacity=0.75) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="Edot", + use_transparency=False, + opacity=1.0, + clim=[0.0, 4.0], + ) + + # pl.remove_scalar_bar("mag") + + pl.show() + + +# - + +if uw.mpi.size == 1: + print(pvmesh.point_data["Visc"].min(), pvmesh.point_data["Visc"].max()) + + diff --git a/main/_sources/Notebooks/Examples-Sandbox-VEP/Ex_Shear_Band_Notch_Benchmark.py b/main/_sources/Notebooks/Examples-Sandbox-VEP/Ex_Shear_Band_Notch_Benchmark.py new file mode 100644 index 0000000..b6323ad --- /dev/null +++ b/main/_sources/Notebooks/Examples-Sandbox-VEP/Ex_Shear_Band_Notch_Benchmark.py @@ -0,0 +1,592 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Spiegelman et al, notch-deformation benchmark +# +# This example is for the notch-localization test of Spiegelman et al. For which they supply a geometry file which gmsh can use to construct meshes at various resolutions. NOTE: we are just demonstrating the mesh here, not the solver configuration / benchmarking. +# +# The `.geo` file is provided and we show how to make this into a `.msh` file and +# how to read that into a `uw.discretisation.Mesh` object. The `.geo` file has header parameters to control the mesh refinement, and we provide a coarse version and the original version. +# +# After that, there is some cell data which we can assign to a data structure on the elements (such as a swarm). + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import petsc4py +from petsc4py import PETSc +import underworld3 as uw +import numpy as np +import sympy +import gmsh +import os + +os.makedirs("meshes", exist_ok=True) + +if uw.mpi.size == 1: + os.makedirs("output", exist_ok=True) +else: + os.makedirs(f"output_np{uw.mpi.size}", exist_ok=True) + + +os.environ["UW_TIMING_ENABLE"] = "1" + +# Define the problem size +# 1 - ultra low res for automatic checking +# 2 - low res problem to play with this notebook +# 3 - medium resolution (be prepared to wait) +# 4 - highest resolution (benchmark case from Spiegelman et al) + +problem_size = 2 + +# For testing and automatic generation of notebook output, +# over-ride the problem size if the UW_TESTING_LEVEL is set + +uw_testing_level = os.environ.get("UW_TESTING_LEVEL") +if uw_testing_level: + try: + problem_size = int(uw_testing_level) + except ValueError: + # Accept the default value + pass + +# - +from underworld3.cython import petsc_discretisation + + +# + +if problem_size <= 1: + cl_1 = 0.25 + cl_2 = 0.15 + cl_2a = 0.1 + cl_3 = 0.25 + cl_4 = 0.15 +elif problem_size == 2: + cl_1 = 0.1 + cl_2 = 0.05 + cl_2a = 0.03 + cl_3 = 0.1 + cl_4 = 0.05 +elif problem_size == 3: + cl_1 = 0.06 + cl_2 = 0.03 + cl_2a = 0.015 + cl_3 = 0.04 + cl_4 = 0.02 +else: + cl_1 = 0.04 + cl_2 = 0.005 + cl_2a = 0.003 + cl_3 = 0.02 + cl_4 = 0.01 + +# The benchmark provides a .geo file. This is the gmsh python +# equivalent (mostly transcribed from the .geo format). The duplicated +# Point2 caused a few problems with the mesh reader at one point. + +if uw.mpi.rank == 0: + gmsh.initialize() + gmsh.option.setNumber("General.Verbosity", 0) + gmsh.model.add("Notch") + + Point1 = gmsh.model.geo.addPoint(-2, -1, 0, cl_1) + # Point2 = gmsh.model.geo.addPoint(-2, -1, 0, cl_1) + Point3 = gmsh.model.geo.addPoint(+2, -1, 0, cl_1) + Point4 = gmsh.model.geo.addPoint(2, -0.75, 0, cl_1) + Point5 = gmsh.model.geo.addPoint(2, 0, 0, cl_1) + Point6 = gmsh.model.geo.addPoint(-2, 0, 0, cl_1) + Point7 = gmsh.model.geo.addPoint(-2, -0.75, 0, cl_1) + Point8 = gmsh.model.geo.addPoint(-0.08333333333329999, -0.75, 0, cl_2) + Point9 = gmsh.model.geo.addPoint(0.08333333333329999, -0.75, 0, cl_2) + Point10 = gmsh.model.geo.addPoint(0.08333333333329999, -0.6666666666667, 0, cl_2) + Point11 = gmsh.model.geo.addPoint(-0.08333333333329999, -0.6666666666667, 0, cl_2) + Point25 = gmsh.model.geo.addPoint(-0.75, 0, 0, cl_4) + Point26 = gmsh.model.geo.addPoint(0.75, 0, 0, cl_4) + Point27 = gmsh.model.geo.addPoint(0, 0, 0, cl_3) + + Line1 = gmsh.model.geo.addLine(Point1, Point3) + Line2 = gmsh.model.geo.addLine(Point3, Point4) + Line3 = gmsh.model.geo.addLine(Point4, Point5) + Line4 = gmsh.model.geo.addLine(Point5, Point26) + Line8 = gmsh.model.geo.addLine(Point26, Point27) + Line9 = gmsh.model.geo.addLine(Point27, Point25) + Line10 = gmsh.model.geo.addLine(Point25, Point6) + Line6 = gmsh.model.geo.addLine(Point6, Point7) + Line7 = gmsh.model.geo.addLine(Point7, Point1) + + Point12 = gmsh.model.geo.addPoint(-0.1033333333333, -0.75, 0, cl_2a) + Point13 = gmsh.model.geo.addPoint(-0.0833333333333, -0.73, 0, cl_2a) + Point14 = gmsh.model.geo.addPoint(-0.0833333333333, -0.686666666666666, 0, cl_2a) + Point15 = gmsh.model.geo.addPoint(-0.0633333333333, -0.666666666666666, 0, cl_2a) + Point16 = gmsh.model.geo.addPoint(0.0633333333333, -0.666666666666666, 0, cl_2a) + Point17 = gmsh.model.geo.addPoint(0.0833333333333, -0.686666666666666, 0, cl_2a) + Point18 = gmsh.model.geo.addPoint(0.0833333333333, -0.73, 0, cl_2a) + Point19 = gmsh.model.geo.addPoint(0.1033333333333, -0.75, 0, cl_2a) + Point20 = gmsh.model.geo.addPoint(-0.103333333333333, -0.73, 0, cl_2a) + Point21 = gmsh.model.geo.addPoint(-0.063333333333333, -0.686666666666666, 0, cl_2a) + Point22 = gmsh.model.geo.addPoint(0.063333333333333, -0.686666666666666, 0, cl_2a) + Point24 = gmsh.model.geo.addPoint(0.103333333333333, -0.73, 0, cl_2a) + + Circle22 = gmsh.model.geo.addCircleArc(Point12, Point20, Point13) + Circle23 = gmsh.model.geo.addCircleArc(Point14, Point21, Point15) + Circle24 = gmsh.model.geo.addCircleArc(Point16, Point22, Point17) + Circle25 = gmsh.model.geo.addCircleArc(Point18, Point24, Point19) + + Line26 = gmsh.model.geo.addLine(Point7, Point12) + Line27 = gmsh.model.geo.addLine(Point13, Point14) + Line28 = gmsh.model.geo.addLine(Point15, Point16) + Line29 = gmsh.model.geo.addLine(Point17, Point18) + Line30 = gmsh.model.geo.addLine(Point19, Point4) + + LineLoop31 = gmsh.model.geo.addCurveLoop( + [ + Line1, + Line2, + -Line30, + -Circle25, + -Line29, + -Circle24, + -Line28, + -Circle23, + -Line27, + -Circle22, + -Line26, + Line7, + ], + ) + + LineLoop33 = gmsh.model.geo.addCurveLoop( + [ + Line6, + Line26, + Circle22, + Line27, + Circle23, + Line28, + Circle24, + Line29, + Circle25, + Line30, + Line3, + Line4, + Line8, + Line9, + Line10, + ], + ) + + Surface32 = gmsh.model.geo.addPlaneSurface([LineLoop31]) + Surface34 = gmsh.model.geo.addPlaneSurface([LineLoop33]) + + gmsh.model.geo.synchronize() + + gmsh.model.addPhysicalGroup(1, [Line1], tag=3, name="Bottom") + gmsh.model.addPhysicalGroup(1, [Line2, Line3], tag=2, name="Right") + gmsh.model.addPhysicalGroup(1, [Line7, Line6], tag=1, name="Left") + gmsh.model.addPhysicalGroup(1, [Line4, Line8, Line9, Line10], tag=4, name="Top") + + gmsh.model.addPhysicalGroup( + 1, + [ + Line26, + Circle22, + Line27, + Circle23, + Line28, + Circle24, + Line29, + Circle25, + Line30, + ], + tag=5, + name="InnerBoundary", + ) + + gmsh.model.addPhysicalGroup(2, [Surface32], tag=100, name="Weak") + gmsh.model.addPhysicalGroup(2, [Surface34], tag=101, name="Strong") + + gmsh.model.mesh.generate(2) + + gmsh.write(f"./meshes/notch_mesh{problem_size}.msh") + gmsh.finalize() +# - + + +from underworld3 import timing + +timing.reset() +timing.start() + +mesh1 = uw.discretisation.Mesh( + f"./meshes/notch_mesh{problem_size}.msh", + simplex=True, + qdegree=3, + markVertices=False, + useRegions=True, + useMultipleTags=True, +) + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh1) + + pl = pv.Plotter(window_size=(1000, 750)) + + pl.add_mesh( + pvmesh, + "Blue", + "wireframe", + opacity=0.5, + ) + # pl.add_points(point_cloud, cmap="coolwarm", render_points_as_spheres=False, point_size=10, opacity=0.66) + + pl.show(cpos="xy") + +swarm = uw.swarm.Swarm(mesh=mesh1) +material = uw.swarm.SwarmVariable( + "M", swarm, size=1, proxy_continuous=False, proxy_degree=0 +) +swarm.populate(fill_param=0) + +v_soln = uw.discretisation.MeshVariable(r"U", mesh1, mesh1.dim, degree=2) +p_soln = uw.discretisation.MeshVariable(r"P", mesh1, 1, degree=1, continuous=True) +p_null = uw.discretisation.MeshVariable(r"P2", mesh1, 1, degree=1, continuous=True) + +edot = uw.discretisation.MeshVariable( + r"\dot\varepsilon", mesh1, 1, degree=1, continuous=True +) +visc = uw.discretisation.MeshVariable(r"\eta", mesh1, 1, degree=1, continuous=False) +stress = uw.discretisation.MeshVariable(r"\sigma", mesh1, 1, degree=1, continuous=True) + +# + [markdown] magic_args="[markdown]" +# This is how we extract cell data from the mesh. We can map it to the swarm data structure and use this to +# build material properties that depend on cell type. +# - + +indexSetW = mesh1.dm.getStratumIS("Weak", 100) +indexSetS = mesh1.dm.getStratumIS("Strong", 101) + + +l = swarm.dm.createLocalVectorFromField("M") +lvec = l.copy() +swarm.dm.restoreField("M") + +lvec.isset(indexSetW, 0.0) +lvec.isset(indexSetS, 1.0) + +with swarm.access(material): + material.data[:, 0] = lvec.array[:] + +# check the mesh if in a notebook / serial + +if True and uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(f"./meshes/notch_mesh{problem_size}.msh") + pvmesh.point_data["eta"] = vis.scalar_fn_to_pv_points(pvmesh, material.sym) + + pl = pv.Plotter(window_size=(1000, 750)) + + # points = np.zeros((mesh1._centroids.shape[0], 3)) + # points[:, 0] = mesh1._centroids[:, 0] + # points[:, 1] = mesh1._centroids[:, 1] + + points = vis.swarm_to_pv_cloud(swarm) + point_cloud = pv.PolyData(points) + + with swarm.access(): + point_cloud.point_data["M"] = material.data.copy() + + + # pl.add_mesh( + # pvmesh, + # cmap="coolwarm", + # edge_color="Black", + # show_edges=True, + # use_transparency=False, + # opacity=0.5, + # ) + + pl.add_points( + point_cloud, + cmap="coolwarm", + render_points_as_spheres=False, + point_size=10, + opacity=0.66, + ) + pl.add_mesh(pvmesh, "Black", "wireframe") + + pl.show(cpos="xy") + + +# ### Check that this mesh can be solved for a simple, linear problem + +# Create Stokes object + +stokes = uw.systems.Stokes( + mesh1, + velocityField=v_soln, + pressureField=p_soln, + solver_name="stokes", + verbose=False, +) + + +# + +# Set solve options here (or remove default values +stokes.petsc_options["ksp_monitor"] = None + +stokes.tolerance = 1.0e-6 +stokes.petsc_options["snes_atol"] = 1e-2 +stokes.bodyforce = sympy.Matrix([0, -0.001]).T + +# stokes.petsc_options["fieldsplit_velocity_ksp_rtol"] = 1e-4 +# stokes.petsc_options["fieldsplit_pressure_ksp_type"] = "gmres" # gmres here for bulletproof +stokes.petsc_options[ + "fieldsplit_pressure_pc_type" +] = "gamg" # can use gasm / gamg / lu here +stokes.petsc_options[ + "fieldsplit_pressure_pc_gasm_type" +] = "basic" # can use gasm / gamg / lu here +stokes.petsc_options[ + "fieldsplit_pressure_pc_gamg_type" +] = "classical" # can use gasm / gamg / lu here +stokes.petsc_options["fieldsplit_pressure_pc_gamg_classical_type"] = "direct" +stokes.petsc_options["fieldsplit_pressure_pc_gamg_esteig_ksp_type"] = "cg" + +# - +stokes.constitutive_model + + +viscosity_L = 999.0 * material.sym[0] + 1.0 + +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.viscosity = viscosity_L +stokes.saddle_preconditioner = 1 / viscosity_L +stokes.penalty = 0.1 + +# Velocity boundary conditions +stokes.add_dirichlet_bc(1.0, "Left", 0) +stokes.add_dirichlet_bc(0, "Left", 1) +stokes.add_dirichlet_bc(-1.0, "Right", 0) +stokes.add_dirichlet_bc(0, "Right", 1) +stokes.add_dirichlet_bc((0.0,), "Bottom", (1,)) +# stokes.add_dirichlet_bc((0.0,), "Top", (1,)) + + +stokes.bodyforce = sympy.Matrix([0, -1]) + + +# + +x, y = mesh1.X + +res = 0.1 +hw = 1000.0 / res +surface_defn_fn = sympy.exp(-((y - 0) ** 2) * hw) +base_defn_fn = sympy.exp(-((y + 1) ** 2) * hw) +edges_fn = sympy.exp(-((x - 2) ** 2) / 0.025) + sympy.exp(-((x + 2) ** 2) / 0.025) +# stokes.bodyforce -= 10000.0 * surface_defn_fn * v_soln.sym[1] * mesh1.CoordinateSystem.unit_j +# - + +stokes.constitutive_model + +# This is a strategy to obtain integrals over the surface (etc) + + +def surface_integral(mesh, uw_function, mask_fn): + calculator = uw.maths.Integral(mesh, uw_function * mask_fn) + value = calculator.evaluate() + + calculator.fn = mask_fn + norm = calculator.evaluate() + + integral = value / norm + + return integral + + +# %% +strain_rate_calc = uw.systems.Projection(mesh1, edot) +strain_rate_calc.uw_function = stokes.Unknowns.Einv2 +strain_rate_calc.smoothing = 1.0e-3 + +viscosity_calc = uw.systems.Projection(mesh1, visc) +viscosity_calc.uw_function = stokes.constitutive_model.Parameters.viscosity +viscosity_calc.smoothing = 1.0e-3 + +stress_calc = uw.systems.Projection(mesh1, stress) +S = stokes.stress_deviator +stress_calc.uw_function = ( + sympy.simplify(sympy.sqrt(((S**2).trace()) / 2)) - p_soln.sym[0] +) +stress_calc.smoothing = 1.0e-3 + +# + +# stokes._setup_terms() + +# + +# stokes._uu_G3 + +# + +# First, we solve the linear problem + +stokes.tolerance = 1e-4 +stokes.petsc_options["snes_atol"] = 1.0e-2 + +# stokes.petsc_options["ksp_rtol"] = 1.0e-4 +# stokes.petsc_options["ksp_atol"] = 1.0e-8 + +# stokes.petsc_options["fieldsplit_pressure_ksp_rtol"] = 1.0e-5 +# stokes.petsc_options["fieldsplit_velocity_ksp_rtol"] = 1.0e-5 + + +stokes.solve(zero_init_guess=True) + +if uw.mpi.rank == 0: + print("Linear solve complete", flush=True) +# + + +C0 = 150 +for i in range(1,10,2): + mu = 0.75 + C = C0 + (1.0 - i / 9) * 15.0 + if uw.mpi.rank == 0: + print(f"Mu - {mu}, C = {C}", flush=True) + + tau_y = C + mu * p_soln.sym[0] + viscosity_L = 999.0 * material.sym[0] + 1.0 + viscosity_Y = tau_y / (2 * stokes.Unknowns.Einv2 + 1.0 / 1000) + viscosity = 1 / (1 / viscosity_Y + 1 / viscosity_L) + + stokes.constitutive_model.Parameters.shear_viscosity_0 = viscosity + stokes.saddle_preconditioner = 1 / viscosity + + # + + # Now use that as the guess for a better job + + # stokes.tolerance = 1e-4 + # stokes.petsc_options["ksp_rtol"] = 1.0e-4 + # stokes.petsc_options["ksp_atol"] = 1.0e-8 + + # stokes.petsc_options["fieldsplit_pressure_ksp_rtol"] = 1.0e-5 + # stokes.petsc_options["fieldsplit_velocity_ksp_rtol"] = 1.0e-5 + # stokes.snes.atol = 1e-3 + + stokes.solve(zero_init_guess=False) + if uw.mpi.rank == 0: + print(f"Completed: Mu - {mu}, C = {C}", flush=True) +# - + + + +# %% +viscosity_calc.uw_function = stokes.constitutive_model.Parameters.viscosity +stress_calc.uw_function = ( + 2 * stokes.constitutive_model.Parameters.viscosity * stokes.Unknowns.Einv2 +) + +# %% +strain_rate_calc.solve() +viscosity_calc.solve() +stress_calc.solve() + +stress.stats() + +## Save data ... +savefile = f"output/notched_beam_mesh_{problem_size}" +mesh1.petsc_save_checkpoint(index=0, meshVars=[p_soln, v_soln, edot], outputPath=savefile) + +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh1) + pvmesh.point_data["sfn"] = vis.scalar_fn_to_pv_points(pvmesh, surface_defn_fn) + pvmesh.point_data["pres"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym) + pvmesh.point_data["edot"] = vis.scalar_fn_to_pv_points(pvmesh, edot.sym) + pvmesh.point_data["eta"] = vis.scalar_fn_to_pv_points(pvmesh, visc.sym) + pvmesh.point_data["str"] = vis.scalar_fn_to_pv_points(pvmesh, stress.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym) + + points = np.zeros((mesh1._centroids.shape[0], 3)) + points[:, 0] = mesh1._centroids[:, 0] + points[:, 1] = mesh1._centroids[:, 1] + point_cloud = pv.PolyData(points) + + with swarm.access(): + point_cloud.point_data["M"] = material.data.copy() + + pl = pv.Plotter(window_size=(1000, 750)) + + # pl.add_arrows(arrow_loc, arrow_length, mag=0.03, opacity=0.75) + + pl.add_mesh( + pvmesh, + cmap="RdYlGn", + scalars="eta", + edge_color="Grey", + show_edges=True, + use_transparency=False, + clim=[0.1, 1.5], + opacity=1.0, + ) + + # pl.add_points( + # point_cloud, + # cmap="coolwarm", + # render_points_as_spheres=False, + # point_size=5, + # opacity=0.1, + # ) + + pl.show(cpos="xy") + +0/0 + +# + +# %% +# surface_defn_fn = sympy.exp(-((y - 0) ** 2) * hw) +# p_surface_ave = surface_integral(mesh1, p_soln.sym[0], surface_defn_fn) +# print(f"Upper surface average P = {p_surface_ave}") + +# + +# surface_defn_fn = sympy.exp(-((y + 1) ** 2) * hw) +# p_surface_ave = surface_integral(mesh1, p_soln.sym[0], surface_defn_fn) +# print(f"Lower surface average P = {p_surface_ave}") + + +# + +# %% +# surface_defn_fn = sympy.exp(-((y + 0.666) ** 2) * hw) +# p_surface_ave = surface_integral(mesh1, edot.sym[0], surface_defn_fn) +# print(f"Edot at 0.666 = {p_surface_ave}") +# - + + +if uw.mpi.size == 1: + print(pvmesh.point_data["eta"].min(), pvmesh.point_data["eta"].max()) + + diff --git a/main/_sources/Notebooks/Examples-Sandbox-VEP/Ex_Shear_Band_Plasticity_PS.py b/main/_sources/Notebooks/Examples-Sandbox-VEP/Ex_Shear_Band_Plasticity_PS.py new file mode 100644 index 0000000..76b73d5 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Sandbox-VEP/Ex_Shear_Band_Plasticity_PS.py @@ -0,0 +1,320 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Flow and Shear banding around a circular inclusion in pure shear +# +# Masuda, T., & Mizuno, N. (1995). Deflection of pure shear viscous flow around a rigid spherical body. Journal of Structural Geology, 17(11), 1615–1620. https://doi.org/10.1016/0191-8141(95)E0016-6 +# +# + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +expt_name = "PS_ShearBand" + +import petsc4py +import underworld3 as uw +import numpy as np + + +# + +import meshio, pygmsh + +# Mesh a 2D pipe with a circular hole + +csize = 0.075 +csize_circle = 0.025 +res = csize_circle + +width = 1.0 +height = 1.0 +radius = 0.2 + +if uw.mpi.rank == 0: + # Generate local mesh on boss process + + with pygmsh.geo.Geometry() as geom: + geom.characteristic_length_max = csize + + inclusion = geom.add_circle( + (0.0, 0.0, 0.0), radius, make_surface=False, mesh_size=csize_circle + ) + domain = geom.add_rectangle( + xmin=-width, + ymin=-height, + xmax=width, + ymax=height, + z=0, + holes=[inclusion], + mesh_size=csize, + ) + + geom.add_physical(domain.surface.curve_loop.curves[0], label="bottom") + geom.add_physical(domain.surface.curve_loop.curves[1], label="right") + geom.add_physical(domain.surface.curve_loop.curves[2], label="top") + geom.add_physical(domain.surface.curve_loop.curves[3], label="left") + + geom.add_physical(inclusion.curve_loop.curves, label="inclusion") + + geom.add_physical(domain.surface, label="Elements") + + geom.generate_mesh(dim=2, verbose=False) + geom.save_geometry("tmp_ps_shear_inclusion.msh") + +# - + + +mesh1 = uw.discretisation.Mesh( + "tmp_ps_shear_inclusion.msh", markVertices=True, useRegions=True, simplex=True +) +mesh1.dm.view() + +# + +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh1) + + pl = pv.Plotter(window_size=(1000, 750)) + + points = np.zeros((mesh1._centroids.shape[0], 3)) + points[:, 0] = mesh1._centroids[:, 0] + points[:, 1] = mesh1._centroids[:, 1] + + point_cloud = pv.PolyData(points) + + # pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.5) + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + use_transparency=False, + opacity=0.5, + ) + + # + + pl.show(cpos="xy") + +# + +# Define some functions on the mesh + +import sympy + +# radius_fn = sympy.sqrt(mesh1.rvec.dot(mesh1.rvec)) # normalise by outer radius if not 1.0 +# unit_rvec = mesh1.rvec / (1.0e-10+radius_fn) + +# Some useful coordinate stuff + +x, y = mesh1.X + +# relative to the centre of the inclusion +r = sympy.sqrt(x**2 + y**2) +th = sympy.atan2(y, x) + +# need a unit_r_vec equivalent + +inclusion_rvec = mesh1.X +inclusion_unit_rvec = inclusion_rvec / inclusion_rvec.dot(inclusion_rvec) +inclusion_unit_rvec = mesh1.vector.to_matrix(inclusion_unit_rvec) + +# Pure shear flow + +vx_ps = mesh1.N.x +vy_ps = -mesh1.N.y + + +# + +v_soln = uw.discretisation.MeshVariable("U", mesh1, mesh1.dim, degree=2) +t_soln = uw.discretisation.MeshVariable("T", mesh1, 1, degree=2) +p_soln = uw.discretisation.MeshVariable("P", mesh1, 1, degree=1) + +vorticity = uw.discretisation.MeshVariable("omega", mesh1, 1, degree=1) +strain_rate_inv2 = uw.discretisation.MeshVariable("eps", mesh1, 1, degree=1) +dev_stress_inv2 = uw.discretisation.MeshVariable("tau", mesh1, 1, degree=1) +node_viscosity = uw.discretisation.MeshVariable("eta", mesh1, 1, degree=1) +r_inc = uw.discretisation.MeshVariable("R", mesh1, 1, degree=1) + + +# + +# Create NS object + +stokes = uw.systems.Stokes( + mesh1, + velocityField=v_soln, + pressureField=p_soln, + verbose=False, + solver_name="stokes", +) + + +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.viscosity = 1 +stokes.saddle_preconditioner = 1 / stokes.constitutive_model.Parameters.viscosity +stokes.penalty = 0.0 + +stokes.petsc_options["ksp_monitor"] = None + + +# + +nodal_strain_rate_inv2 = uw.systems.Projection( + mesh1, strain_rate_inv2, solver_name="edot_II" +) +nodal_strain_rate_inv2.add_dirichlet_bc(1.0, "top", 0) +nodal_strain_rate_inv2.add_dirichlet_bc(1.0, "bottom", 0) +nodal_strain_rate_inv2.uw_function = stokes.Unknowns.Einv2 +nodal_strain_rate_inv2.smoothing = 1.0e-3 +nodal_strain_rate_inv2.petsc_options.delValue("ksp_monitor") + +nodal_tau_inv2 = uw.systems.Projection(mesh1, dev_stress_inv2, solver_name="stress_II") + +S = stokes.stress_deviator +nodal_tau_inv2.uw_function = ( + sympy.simplify(sympy.sqrt(((S**2).trace()) / 2)) - p_soln.sym[0] +) +nodal_tau_inv2.smoothing = 1.0e-3 +nodal_tau_inv2.petsc_options.delValue("ksp_monitor") + +nodal_visc_calc = uw.systems.Projection(mesh1, node_viscosity, solver_name="visc") +nodal_visc_calc.uw_function = stokes.constitutive_model.Parameters.viscosity +nodal_visc_calc.smoothing = 1.0e-3 +nodal_visc_calc.petsc_options.delValue("ksp_monitor") + + +# + +# Set solve options here (or remove default values +# stokes.petsc_options.getAll() + +# Constant visc + +stokes.penalty = 0.0 +stokes.bodyforce = 1.0e-32 * mesh1.N.i + +hw = 1000.0 / res +surface_defn_fn = sympy.exp(-(((r - radius) / radius) ** 2) * hw) +stokes.bodyforce -= ( + 1.0e6 * surface_defn_fn * v_soln.sym.dot(inclusion_unit_rvec) * inclusion_unit_rvec +) + +# Velocity boundary conditions + +# stokes.add_dirichlet_bc((0.0, 0.0), "inclusion", (0, 1)) +stokes.add_dirichlet_bc((vx_ps, vy_ps), "top", (0, 1)) +stokes.add_dirichlet_bc((vx_ps, vy_ps), "bottom", (0, 1)) +stokes.add_dirichlet_bc((vx_ps, vy_ps), "left", (0, 1)) +stokes.add_dirichlet_bc((vx_ps, vy_ps), "right", (0, 1)) + + +# + +# linear solve first + +stokes.solve() +# + +# Now introduce the non-linearity once we have an initial strain rate + +mu = 0.25 +tau_y = sympy.Max(3.5 + mu * stokes.p.sym[0], 0.1) +viscosity = sympy.Min(tau_y / (2 * stokes.Unknowns.Einv2 + 0.01), 1.0) +# viscosity = 100 * (0.01 + stokes._Einv2) +stokes.constitutive_model.Parameters.viscosity = viscosity +stokes.saddle_preconditioner = 1 / viscosity + + +# + +# Approach the required value by shifting the parameters + +for i in range(1): #5 + mu = 0.25 + C = 2.5 + (1 - i / 4) * 1.0 + print(f"Mu - {mu}, C = {C}") + tau_y = sympy.Max(C + mu * stokes.p.sym[0], 0.1) + viscosity = sympy.Min(tau_y / (2 * stokes.Unknowns.Einv2 + 0.01), 1.0) + # viscosity = 100 * (0.01 + stokes._Einv2) + stokes.constitutive_model.Parameters.viscosity = viscosity + stokes.saddle_preconditioner = 1 / viscosity + stokes.solve(zero_init_guess=False) +# - + +nodal_tau_inv2.uw_function = ( + stokes.constitutive_model.Parameters.viscosity * stokes.Unknowns.Einv2 +) +nodal_tau_inv2.solve() +nodal_visc_calc.uw_function = stokes.constitutive_model.Parameters.viscosity +nodal_visc_calc.solve() +nodal_strain_rate_inv2.solve() + + +# + +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh1) + pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym) + pvmesh.point_data["Edot"] = vis.scalar_fn_to_pv_points(pvmesh, strain_rate_inv2.sym) + pvmesh.point_data["Visc"] = vis.scalar_fn_to_pv_points(pvmesh, node_viscosity.sym) + pvmesh.point_data["Str"] = vis.scalar_fn_to_pv_points(pvmesh, dev_stress_inv2.sym) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) + pvmesh.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh, v_soln.sym.dot(v_soln.sym)) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym) + + # point sources at cell centres + points = np.zeros((mesh1._centroids.shape[0], 3)) + points[:, 0] = mesh1._centroids[:, 0] + points[:, 1] = mesh1._centroids[:, 1] + point_cloud = pv.PolyData(points) + + pvstream = pvmesh.streamlines_from_source( + point_cloud, vectors="V", integration_direction="both", max_steps=100 + ) + + pl = pv.Plotter(window_size=(1000, 500)) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.1, opacity=0.75) + + # pl.add_points(point_cloud, cmap="coolwarm", + # render_points_as_spheres=False, + # point_size=10, opacity=0.66 + # ) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="Edot", + use_transparency=False, + opacity=1.0, + ) # clim=[0.0,1.0]) + + # pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.75) + # pl.add_mesh(pvstream) + + # pl.remove_scalar_bar("mag") + + pl.show() +# - + + diff --git a/main/_sources/Notebooks/Examples-Sandbox-VEP/Ex_Shear_Band_Plasticity_SS.py b/main/_sources/Notebooks/Examples-Sandbox-VEP/Ex_Shear_Band_Plasticity_SS.py new file mode 100644 index 0000000..48ca433 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Sandbox-VEP/Ex_Shear_Band_Plasticity_SS.py @@ -0,0 +1,289 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Shear bands around a circular inclusion in a simple shear flow +# +# No slip conditions +# +# + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +expt_name = "ShearBand" + +import petsc4py +import underworld3 as uw +import numpy as np + + +# + +import meshio, pygmsh + +# Mesh a 2D pipe with a circular hole + +csize = 0.075 +csize_circle = 0.025 +res = csize_circle + +width = 3.0 +height = 1.0 +radius = 0.1 + +if uw.mpi.rank == 0: + # Generate local mesh on boss process + + with pygmsh.geo.Geometry() as geom: + geom.characteristic_length_max = csize + + inclusion = geom.add_circle( + (0.0, 0.0, 0.0), radius, make_surface=False, mesh_size=csize_circle + ) + domain = geom.add_rectangle( + xmin=-width / 2, + ymin=-height / 2, + xmax=width / 2, + ymax=height / 2, + z=0, + holes=[inclusion], + mesh_size=csize, + ) + + geom.add_physical(domain.surface.curve_loop.curves[0], label="bottom") + geom.add_physical(domain.surface.curve_loop.curves[1], label="right") + geom.add_physical(domain.surface.curve_loop.curves[2], label="top") + geom.add_physical(domain.surface.curve_loop.curves[3], label="left") + + geom.add_physical(inclusion.curve_loop.curves, label="inclusion") + + geom.add_physical(domain.surface, label="Elements") + + geom.generate_mesh(dim=2, verbose=False) + geom.save_geometry("tmp_shear_inclusion.msh") + +# - + + +mesh1 = uw.discretisation.Mesh("tmp_shear_inclusion.msh", simplex=True) + +# + +# Define some functions on the mesh + +import sympy + +# radius_fn = sympy.sqrt(mesh1.rvec.dot(mesh1.rvec)) # normalise by outer radius if not 1.0 +# unit_rvec = mesh1.rvec / (1.0e-10+radius_fn) + +# Some useful coordinate stuff + +x, y = mesh1.X + +# relative to the centre of the inclusion +r = sympy.sqrt(x**2 + y**2) +th = sympy.atan2(y, x) + +# need a unit_r_vec equivalent + +inclusion_rvec = mesh1.X +inclusion_unit_rvec = inclusion_rvec / inclusion_rvec.dot(inclusion_rvec) +inclusion_unit_rvec = mesh1.vector.to_matrix(inclusion_unit_rvec) + + +# + +v_soln = uw.discretisation.MeshVariable("U", mesh1, mesh1.dim, degree=2) +p_soln = uw.discretisation.MeshVariable("P", mesh1, 1, degree=1, continuous=False) +p_cont = uw.discretisation.MeshVariable("Pc", mesh1, 1, degree=1, continuous=True) +p_null = uw.discretisation.MeshVariable(r"P2", mesh1, 1, degree=1, continuous=True) + +vorticity = uw.discretisation.MeshVariable("omega", mesh1, 1, degree=1) +strain_rate_inv2 = uw.discretisation.MeshVariable("eps", mesh1, 1, degree=1) +dev_stress_inv2 = uw.discretisation.MeshVariable("tau", mesh1, 1, degree=1) +node_viscosity = uw.discretisation.MeshVariable("eta", mesh1, 1, degree=1) +r_inc = uw.discretisation.MeshVariable("R", mesh1, 1, degree=1) + + +# + +# Create NS object + +stokes = uw.systems.Stokes( + mesh1, + velocityField=v_soln, + pressureField=p_soln, + verbose=False, + solver_name="stokes", +) + + +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.viscosity = 1 +stokes.saddle_preconditioner = 1 / stokes.constitutive_model.Parameters.viscosity +stokes.penalty = 0.0 + +stokes.petsc_options["ksp_monitor"] = None +stokes.petsc_options["snes_atol"] = 0.001 + + +# + +nodal_strain_rate_inv2 = uw.systems.Projection( + mesh1, strain_rate_inv2, solver_name="edot_II" +) +nodal_strain_rate_inv2.add_dirichlet_bc(1.0, "top", 0) +nodal_strain_rate_inv2.add_dirichlet_bc(1.0, "bottom", 0) +nodal_strain_rate_inv2.uw_function = stokes.Unknowns.Einv2 +nodal_strain_rate_inv2.smoothing = 1.0e-3 +nodal_strain_rate_inv2.petsc_options.delValue("ksp_monitor") + +nodal_tau_inv2 = uw.systems.Projection(mesh1, dev_stress_inv2, solver_name="stress_II") +S = stokes.stress_deviator +nodal_tau_inv2.uw_function = ( + sympy.simplify(sympy.sqrt(((S**2).trace()) / 2)) - p_soln.sym[0] +) +nodal_tau_inv2.smoothing = 1.0e-3 +nodal_tau_inv2.petsc_options.delValue("ksp_monitor") + +nodal_visc_calc = uw.systems.Projection(mesh1, node_viscosity, solver_name="visc") +nodal_visc_calc.uw_function = stokes.constitutive_model.Parameters.viscosity +nodal_visc_calc.smoothing = 1.0e-3 +nodal_visc_calc.petsc_options.delValue("ksp_monitor") + +# nodal_pres_calc = uw.systems.Projection(mesh1, p_cont, solver_name="pres") +# nodal_pres_calc.uw_function = p_soln.sym[0] +# nodal_pres_calc.smoothing = 1.0e-3 +# nodal_pres_calc.petsc_options.delValue("ksp_monitor") + + +# + +# Set solve options here (or remove default values +# stokes.petsc_options.getAll() + +# Constant visc + +stokes.penalty = 0.0 +stokes.bodyforce = 1.0e-32 * mesh1.CoordinateSystem.unit_e_1 + +hw = 1000.0 / res +surface_defn_fn = sympy.exp(-(((r - radius) / radius) ** 2) * hw) +# stokes.bodyforce -= 1.0e6 * surface_defn_fn * v_soln.sym.dot(inclusion_unit_rvec) * inclusion_unit_rvec + +# Velocity boundary conditions + +stokes.add_dirichlet_bc((0.0, 0.0), "inclusion", (0, 1)) +stokes.add_dirichlet_bc((1.0, 0.0), "top", (0, 1)) +stokes.add_dirichlet_bc((-1.0, 0.0), "bottom", (0, 1)) +stokes.add_dirichlet_bc(0.0, "left", 1) +stokes.add_dirichlet_bc(0.0, "right", 1) + + +# + +# linear solve first + +stokes.solve() + +# + +# Approach the required non-linear value by gradually adjusting the parameters + +steps = 8 +for i in range(steps): + mu = 0.5 + C = 2.5 + (steps - i) * 0.33 + print(f"Mu - {mu}, C = {C}") + tau_y = sympy.Max(C + mu * stokes.p.sym[0], 0.1) + viscosity = sympy.Min(tau_y / (2 * stokes.Unknowns.Einv2 + 0.01), 1.0) + # viscosity = 100 * (0.01 + stokes._Einv2) + stokes.constitutive_model.Parameters.viscosity = viscosity + stokes.saddle_preconditioner = 1 / viscosity + stokes.solve(zero_init_guess=False) + +# - + + +nodal_tau_inv2.uw_function = ( + stokes.constitutive_model.Parameters.viscosity * stokes.Unknowns.Einv2 +) +nodal_tau_inv2.solve() +nodal_visc_calc.uw_function = stokes.constitutive_model.Parameters.viscosity +nodal_visc_calc.solve() +nodal_strain_rate_inv2.solve() +# nodal_pres_calc.solve() + +with mesh1.access(): + print(v_soln.data) + +# + +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh1) + pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_cont.sym) + pvmesh.point_data["Edot"] = vis.scalar_fn_to_pv_points(pvmesh, strain_rate_inv2.sym) + pvmesh.point_data["Visc"] = vis.scalar_fn_to_pv_points(pvmesh, node_viscosity.sym) + pvmesh.point_data["Str"] = vis.scalar_fn_to_pv_points(pvmesh, dev_stress_inv2.sym) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) + pvmesh.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh, v_soln.sym.dot(v_soln.sym)) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym) + + + # point sources at cell centres + + subsample = 10 + points = np.zeros((mesh1._centroids[::subsample].shape[0], 3)) + points[:, 0] = mesh1._centroids[::subsample, 0] + points[:, 1] = mesh1._centroids[::subsample, 1] + point_cloud = pv.PolyData(points) + + pvstream = pvmesh.streamlines_from_source( + point_cloud, vectors="V", integration_direction="both", max_steps=100 + ) + + pl = pv.Plotter(window_size=(1000, 500)) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=0.1, opacity=0.75) + pl.camera_position = "xy" + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Grey", + show_edges=True, + clim=[1.0, 2.0], + scalars="Edot", + use_transparency=False, + opacity=1.0, + ) + + # pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.75) + pl.add_mesh(pvstream) + + # pl.remove_scalar_bar("mag") + + pl.show() +# - +pvmesh.point_data["Visc"].min(), pvmesh.point_data["Visc"].max() + +pvmesh.point_data["P"].min(), pvmesh.point_data["P"].max() # cf 4.26 + +pvmesh.point_data["Str"].min(), pvmesh.point_data["Str"].max() + +pvmesh.point_data["Edot"].min(), pvmesh.point_data["Edot"].max() + +pvmesh.point_data["V"].min(), pvmesh.point_data["V"].max() + + diff --git a/main/_sources/Notebooks/Examples-Sandbox-VEP/Ex_Sheared_Layer_Elastic.py b/main/_sources/Notebooks/Examples-Sandbox-VEP/Ex_Sheared_Layer_Elastic.py new file mode 100644 index 0000000..d145450 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Sandbox-VEP/Ex_Sheared_Layer_Elastic.py @@ -0,0 +1,698 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Validate constitutive models +# +# Simple shear with material defined by particle swarm (based on inclusion model), position, pressure, strain rate etc. Check the implementation of the Jacobians using various non-linear terms. +# +# Check elastic stress terms +# + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import os + +os.environ["UW_TIMING_ENABLE"] = "1" + +import petsc4py +import underworld3 as uw +import numpy as np +import sympy +import pyvista as pv +import vtk + +from underworld3 import timing + +resolution = uw.options.getReal("model_resolution", default=0.05) +mu = uw.options.getInt("mu", default=0.5) +maxsteps = uw.options.getInt("max_steps", default=500) + + +## Define units here and physical timestep numbers etc. + +observation_timescale = 0.01 + + +# + +# Mesh a 2D pipe with a circular hole + +mesh1 = uw.meshing.UnstructuredSimplexBox( + minCoords=(-1.5, -0.5), + maxCoords=(+1.5, +0.5), + cellSize=resolution, +) + + +# + + +mesh1.dm.view() + +## build periodic mesh (mesh1) +# uw.cython.petsc_discretisation.petsc_dm_set_periodicity( +# mesh1.dm, [0.1, 0.0], [-1.5, 0.0], [1.5, 0.0]) + +# mesh1.dm.view() +# - + +v_soln = uw.discretisation.MeshVariable("U", mesh1, mesh1.dim, degree=2) +p_soln = uw.discretisation.MeshVariable( + "P", mesh1, 1, vtype=uw.VarType.SCALAR, degree=1, continuous=True +) +Stress = uw.discretisation.MeshVariable( + r"Stress", + mesh1, + (2, 2), + vtype=uw.VarType.SYM_TENSOR, + degree=2, + continuous=True, + varsymbol=r"{\sigma}", +) +work = uw.discretisation.MeshVariable( + "W", mesh1, 1, vtype=uw.VarType.SCALAR, degree=2, continuous=True +) +strain_rate_inv2 = uw.discretisation.MeshVariable( + "eps_dot", mesh1, 1, degree=2, varsymbol=r"{\dot\varepsilon}" +) +strain_rate_inv2_pl = uw.discretisation.MeshVariable( + "eps_dot_pl", mesh1, 1, degree=2, varsymbol=r"{\dot\varepsilon_{pl}}" +) +dev_stress_inv2 = uw.discretisation.MeshVariable("tau", mesh1, 1, degree=2) + +mesh1.view() + +# + +swarm = uw.swarm.Swarm(mesh=mesh1, recycle_rate=5) + +material = uw.swarm.SwarmVariable( + "M", + swarm, + size=1, + vtype=uw.VarType.SCALAR, + proxy_continuous=True, + proxy_degree=1, + dtype=int, +) + +strain = uw.swarm.SwarmVariable( + "Strain", + swarm, + size=1, + vtype=uw.VarType.SCALAR, + proxy_continuous=True, + proxy_degree=2, + varsymbol=r"\varepsilon", + dtype=float, +) + +stress_star_p = uw.swarm.SwarmVariable( + r"stress_p", + swarm, + (2, 2), + vtype=uw.VarType.SYM_TENSOR, + proxy_continuous=True, + proxy_degree=2, + varsymbol=r"{\sigma^{*}_{p}}", +) + +swarm.populate(fill_param=2) + +stress_star_update_dt = uw.swarm.Lagrangian_Updater( + swarm, Stress.sym, [stress_star_p], dt_physical=observation_timescale +) +# - + +# Some useful coordinate stuff +x, y = mesh1.X + + +with swarm.access(strain, material), mesh1.access(): + XX = swarm.particle_coordinates.data[:, 0] + YY = swarm.particle_coordinates.data[:, 1] + mask = (1.0 - (YY * 2) ** 8) * (1 - (2 * XX / 3) ** 6) + material.data[(XX**2 + YY**2 < 0.01), 0] = 1 + strain.data[:, 0] = ( + 0.01 * np.random.random(swarm.particle_coordinates.data.shape[0]) * mask + ) +# + +# Create Solver object + +stokes = uw.systems.Stokes( + mesh1, + velocityField=v_soln, + pressureField=p_soln, + verbose=False, + solver_name="stokes", +) + +viscosity_L = sympy.Piecewise( + (1, material.sym[0] > 0.5), + (1000, True), +) + + +# - + +stokes.constitutive_model = uw.constitutive_models.ViscoElasticPlasticFlowModel( + u=v_soln, flux_dt=stress_star_update_dt +) + +stokes.constitutive_model.Parameters.shear_viscosity_0 = viscosity_L +stokes.constitutive_model.Parameters.shear_modulus = sympy.sympify(100) +stokes.constitutive_model.Parameters.stress_star = stress_star_p.sym +stokes.constitutive_model.Parameters.dt_elastic = sympy.sympify(observation_timescale) + + +stokes.constitutive_model + +sigma_projector = uw.systems.Tensor_Projection( + mesh1, tensor_Field=Stress, scalar_Field=work +) +sigma_projector.uw_function = stokes.stress_1d + +# + +nodal_strain_rate_inv2 = uw.systems.Projection( + mesh1, strain_rate_inv2, solver_name="edot_II" +) + +nodal_strain_rate_inv2.uw_function = stokes._Einv2 +nodal_strain_rate_inv2.smoothing = 1.0e-3 + +nodal_tau_inv2 = uw.systems.Projection(mesh1, dev_stress_inv2, solver_name="stress_II") +nodal_tau_inv2.uw_function = 2 * stokes.constitutive_model.viscosity * stokes._Einv2 +nodal_tau_inv2.smoothing = 1.0e-3 + + +# + +# Set solve options here (or remove default values +# stokes.petsc_options.getAll() + +# Constant visc + +stokes.penalty = 1.0 +stokes.tolerance = 1.0e-4 + +# Velocity boundary conditions + +stokes.add_dirichlet_bc((0.0, 0.0), "Inclusion", (0, 1)) +stokes.add_dirichlet_bc((1.0, 0.0), "Top", (0, 1)) +stokes.add_dirichlet_bc((-1.0, 0.0), "Bottom", (0, 1)) +stokes.add_dirichlet_bc((0.0), "Left", (1)) +stokes.add_dirichlet_bc((0.0), "Right", (1)) + +# - +stress_star_update_dt.psi_star[0].sym + +stokes.solve() + +stokes.constitutive_model.Parameters.strainrate_inv_II_min = 0.00001 +stokes.constitutive_model.Parameters.yield_stress = 50 + +stokes + +stokes.stress[0, 0] + +# + +nodal_strain_rate_inv2.solve() + +sigma_projector.uw_function = stokes.stress_deviator +sigma_projector.solve() +# - +with swarm.access(stress_star_p), mesh1.access(): + stress_star_p.data[ + ... + ] = 0.0 # Stress.rbf_interpolate(swarm.particle_coordinates.data) + + +timing.reset() +timing.start() + +print("Setup terms", flush=True) + +stokes._setup_terms() + +stokes.stress[0, 0] + + +stokes.solve(zero_init_guess=False, verbose=True) +timing.print_table(display_fraction=1) +print(stokes._u.max(), stokes._p.max()) + + +# + +nodal_strain_rate_inv2.uw_function = stokes._Einv2 +nodal_strain_rate_inv2.solve() + +S = stokes.stress_deviator +nodal_tau_inv2.uw_function = stokes.constitutive_model.viscosity * 2 * stokes._Einv2 +nodal_tau_inv2.solve() +# - + +stokes.constitutive_model.flux_dt + +# + +# check it - NOTE - for the periodic mesh, points which have crossed the coordinate sheet are plotted somewhere +# unexpected. This is a limitation we are stuck with for the moment. + +if uw.mpi.size == 1: + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh1) + + pvpoints = pvmesh.points[:, 0:2] + usol = v_soln.rbf_interpolate(pvpoints) + + pvmesh.point_data["P"] = p_soln.rbf_interpolate(pvpoints) + pvmesh.point_data["Edot"] = strain_rate_inv2.rbf_interpolate(pvpoints) + pvmesh.point_data["Strs"] = dev_stress_inv2.rbf_interpolate(pvpoints) + pvmesh.point_data["Mat"] = material.rbf_interpolate(pvpoints) + pvmesh.point_data["Strn"] = strain._meshVar.rbf_interpolate(pvpoints) + pvmesh.point_data["SStar"] = stress_star_p._meshVar.rbf_interpolate(pvpoints) + + # Velocity arrows + + v_vectors = np.zeros_like(pvmesh.points) + v_vectors[:, 0:2] = v_soln.rbf_interpolate(pvpoints) + + # Points (swarm) + + with swarm.access(): + points = np.zeros((swarm.data.shape[0], 3)) + points[:, 0] = swarm.data[:, 0] + points[:, 1] = swarm.data[:, 1] + point_cloud = pv.PolyData(points) + point_cloud.point_data["strain"] = strain.data[:, 0] + + pl = pv.Plotter(window_size=(500, 500)) + + pl.add_arrows(pvmesh.points, v_vectors, mag=0.1, opacity=0.75) + # pl.camera_position = "xy" + + pl.add_mesh( + pvmesh, + cmap="Blues", + edge_color="Grey", + show_edges=True, + # clim=[0.0,1.0], + scalars="Strs", + use_transparency=False, + opacity=0.5, + ) + + # pl.add_points(point_cloud, colormap="coolwarm", scalars="strain", point_size=10.0, opacity=0.5) + + pl.camera.SetPosition(0.0, 0.0, 3.0) + pl.camera.SetFocalPoint(0.0, 0.0, 0.0) + pl.camera.SetClippingRange(1.0, 8.0) + + pl.show() + + +# - +def return_points_to_domain(coords): + new_coords = coords.copy() + new_coords[:, 0] = (coords[:, 0] + 1.5) % 3 - 1.5 + return new_coords + + +ts = 0 + +stress_star_update_dt.view() + +# + +expt_name = f"shear_band_sw_nonp_{mu}" + +for step in range(0, 75): + stokes.solve(zero_init_guess=False) + + delta_t = 0.01 + + nodal_strain_rate_inv2.uw_function = sympy.Max( + 0.0, + stokes._Einv2 + - 0.5 + * stokes.constitutive_model.Parameters.yield_stress + / stokes.constitutive_model.Parameters.shear_viscosity_0, + ) + nodal_strain_rate_inv2.solve() + + with mesh1.access(strain_rate_inv2_pl): + strain_rate_inv2_pl.data[...] = strain_rate_inv2.data.copy() + + nodal_strain_rate_inv2.uw_function = stokes._Einv2 + nodal_strain_rate_inv2.solve() + + S = stokes.stress_deviator + nodal_tau_inv2.uw_function = sympy.simplify(sympy.sqrt(((S**2).trace()) / 2)) + nodal_tau_inv2.solve() + + if uw.mpi.rank == 0: + print(f"Stress Inv II - {dev_stress_inv2.mean()}") + + sigma_projector.solve() + stress_star_update_dt.update(dt=delta_t, evalf=True) + + with swarm.access(strain), mesh1.access(): + XX = swarm.particle_coordinates.data[:, 0] + YY = swarm.particle_coordinates.data[:, 1] + mask = (2 * XX / 3) ** 4 # * 1.0 - (YY * 2)**8 + strain.data[:, 0] += ( + delta_t * mask * strain_rate_inv2_pl.rbf_interpolate(swarm.data)[:, 0] + - 0.1 * delta_t + ) + strain_dat = ( + delta_t * mask * strain_rate_inv2_pl.rbf_interpolate(swarm.data)[:, 0] + ) + + if uw.mpi.rank == 0: + print(f"Sstar[0,0] = {(np.sqrt(stress_star_p[0,0].data[:]**2)).mean()}") + print(f"Sstar[1,0] = {(np.sqrt(stress_star_p[0,1].data[:]**2)).mean()}") + print(f"Sstar[1,1] = {(np.sqrt(stress_star_p[1,1].data[:]**2)).mean()}") + + mesh1.write_timestep( + expt_name, + meshUpdates=False, + meshVars=[p_soln, v_soln, strain_rate_inv2_pl], + outputPath="output", + index=ts, + ) + + swarm.save(f"{expt_name}.swarm.{ts}.h5") + strain.save(f"{expt_name}.strain.{ts}.h5") + + # Update the swarm locations + swarm.advection( + v_soln.sym, delta_t=delta_t, restore_points_to_domain_func=None, evalf=True + ) + + if uw.mpi.rank == 0: + print("Timestep {}, dt {}".format(step, delta_t)) + + ts += 1 +# - + + +stokes.constitutive_model.stress_projection()[0, 0] + +stokes.stress[0, 0] + +# + +# check it - NOTE - for the periodic mesh, points which have crossed the coordinate sheet are plotted somewhere +# unexpected. This is a limitation we are stuck with for the moment. + +if uw.mpi.size == 1: + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh1) + + pvpoints = pvmesh.points[:, 0:2] + usol = v_soln.rbf_interpolate(pvpoints) + + pvmesh.point_data["P"] = p_soln.rbf_interpolate(pvpoints) + pvmesh.point_data["Edot"] = strain_rate_inv2.rbf_interpolate(pvpoints) + pvmesh.point_data["Strs"] = dev_stress_inv2.rbf_interpolate(pvpoints) + pvmesh.point_data["Mat"] = material.rbf_interpolate(pvpoints) + pvmesh.point_data["Strn"] = strain._meshVar.rbf_interpolate(pvpoints) + pvmesh.point_data["SStar"] = uw.function.evalf(stress_star_p[1, 1].sym, pvpoints) + + # Velocity arrows + + v_vectors = np.zeros_like(pvmesh.points) + v_vectors[:, 0:2] = v_soln.rbf_interpolate(pvpoints) + + # Points (swarm) + + with swarm.access(): + points = np.zeros((swarm.data.shape[0], 3)) + points[:, 0] = swarm.data[:, 0] + points[:, 1] = swarm.data[:, 1] + point_cloud = pv.PolyData(points) + point_cloud.point_data["strain"] = strain.data[:, 0] + + pl = pv.Plotter(window_size=(500, 500)) + + pl.add_arrows(pvmesh.points, v_vectors, mag=0.1, opacity=0.75) + # pl.camera_position = "xy" + + pl.add_mesh( + pvmesh, + cmap="Blues", + edge_color="Grey", + show_edges=True, + # clim=[0.0,1.0], + scalars="Mat", + use_transparency=False, + opacity=0.5, + ) + + # pl.add_points(point_cloud, colormap="coolwarm", scalars="strain", point_size=10.0, opacity=0.5) + + pl.camera.SetPosition(0.0, 0.0, 3.0) + pl.camera.SetFocalPoint(0.0, 0.0, 0.0) + pl.camera.SetClippingRange(1.0, 8.0) + + pl.show() +# - +stress_star_p._meshVar.min() + +nodal_tau_inv2.snes.cancelMonitor() + + +0 / 0 + +# + +# Adams / Bashforth & Adams Moulton ... + +s = sympy.Symbol(r"\sigma") +s1 = sympy.Symbol(r"\sigma^*") +s2 = sympy.Symbol(r"\sigma^**") +dt = sympy.Symbol(r"\Delta t") +mu = sympy.Symbol(r"\mu") +eta = sympy.Symbol(r"\eta") +edot = sympy.Symbol(r"\dot\varepsilon") +tr = sympy.Symbol(r"t_r") + +sdot1 = (s - s1) / dt +sdot2 = (3 * s - 4 * s1 + s2) / (2 * dt) +# - + + +display(sdot1) +display(sdot2) +Seq1 = sympy.Equality(sympy.simplify(sdot1 / (2 * mu) + s / (2 * eta)), edot) +display(Seq1) +sympy.simplify(sympy.solve(Seq1, s)[0]) + +eta_eff_1 = sympy.simplify(eta * mu * dt / (mu * dt + eta)) +display(eta_eff_1) +a = sympy.simplify(2 * eta * sympy.solve(Seq1, s)[0] / (2 * eta_eff_1)) +tau_1 = a.subs(eta / mu, tr) +tau_1 + +Seq2 = sympy.Equality(sympy.simplify(sdot2 / (2 * mu) + s / (2 * eta)), edot) +display(Seq2) +sympy.simplify(sympy.solve(Seq2, s)[0]) + +eta_eff_2 = sympy.simplify(2 * eta * mu * dt / (2 * mu * dt + 3 * eta)) +display(eta_eff_2) +sympy.simplify(2 * eta * sympy.solve(Seq2, s)[0] / (2 * eta_eff_2)) + +a = sympy.simplify(2 * eta * sympy.solve(Seq2, s)[0] / (2 * eta_eff_2)) +tau_2 = a.expand().subs(eta / mu, tr) + +tau_2 + +# + +# 0/0 +# - + + +stokes.constitutive_model.Parameters.yield_stress.subs( + ((strain.sym[0], 0.25), (y, 0.0)) +) + +stokes.constitutive_model.Parameters.viscosity + + +def return_points_to_domain(coords): + new_coords = coords.copy() + new_coords[:, 0] = (coords[:, 0] + 1.5) % 3 - 1.5 + return new_coords + + +ts = 0 + +# + +expt_name = f"output/shear_band_sw_nonp_{mu}" + +for step in range(0, 10): + stokes.solve(zero_init_guess=False) + + delta_t = stokes.estimate_dt() + + nodal_strain_rate_inv2.uw_function = sympy.Max( + 0.0, + stokes._Einv2 + - 0.5 + * stokes.constitutive_model.Parameters.yield_stress + / stokes.constitutive_model.Parameters.shear_viscosity_0, + ) + nodal_strain_rate_inv2.solve() + + with mesh1.access(strain_rate_inv2_pl): + strain_rate_inv2_pl.data[...] = strain_rate_inv2.data.copy() + + nodal_strain_rate_inv2.uw_function = stokes._Einv2 + nodal_strain_rate_inv2.solve() + + with swarm.access(strain), mesh1.access(): + XX = swarm.particle_coordinates.data[:, 0] + YY = swarm.particle_coordinates.data[:, 1] + mask = (2 * XX / 3) ** 4 # * 1.0 - (YY * 2)**8 + strain.data[:, 0] += ( + delta_t * mask * strain_rate_inv2_pl.rbf_interpolate(swarm.data)[:, 0] + - 0.1 * delta_t + ) + strain_dat = ( + delta_t * mask * strain_rate_inv2_pl.rbf_interpolate(swarm.data)[:, 0] + ) + print( + f"dStrain / dt = {delta_t * (mask * strain_rate_inv2_pl.rbf_interpolate(swarm.data)[:,0]).mean()}, {delta_t}" + ) + + mesh1.write_timestep_xdmf( + f"{expt_name}", + meshUpdates=False, + meshVars=[p_soln, v_soln, strain_rate_inv2_pl], + swarmVars=[strain], + index=ts, + ) + + swarm.save(f"{expt_name}.swarm.{ts}.h5") + strain.save(f"{expt_name}.strain.{ts}.h5") + + # Update the swarm locations + swarm.advection(v_soln.sym, delta_t=delta_t, restore_points_to_domain_func=None) + + if uw.mpi.rank == 0: + print("Timestep {}, dt {}".format(step, delta_t)) + + ts += 1 + + + +# + +# nodal_visc_calc.uw_function = sympy.log(stokes.constitutive_model.Parameters.viscosity) +# nodal_visc_calc.solve() + +# yield_stress_calc.uw_function = stokes.constitutive_model.Parameters.yield_stress +# yield_stress_calc.solve() + +nodal_tau_inv2.uw_function = ( + 2 * stokes.constitutive_model.Parameters.viscosity * stokes._Einv2 +) +nodal_tau_inv2.solve() + +# + +# check it - NOTE - for the periodic mesh, points which have crossed the coordinate sheet are plotted somewhere +# unexpected. This is a limitation we are stuck with for the moment. + +if uw.mpi.size == 1: + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh1) + + pvpoints = pvmesh.points[:, 0:2] + usol = v_soln.rbf_interpolate(pvpoints) + + pvmesh.point_data["P"] = p_soln.rbf_interpolate(pvpoints) + pvmesh.point_data["Edot"] = strain_rate_inv2.rbf_interpolate(pvpoints) + # pvmesh.point_data["Visc"] = np.exp(node_viscosity.rbf_interpolate(pvpoints)) + pvmesh.point_data["Edotp"] = strain_rate_inv2_pl.rbf_interpolate(pvpoints) + pvmesh.point_data["Strs"] = dev_stress_inv2.rbf_interpolate(pvpoints) + # pvmesh.point_data["StrY"] = yield_stress.rbf_interpolate(pvpoints) + # pvmesh.point_data["dStrY"] = pvmesh.point_data["StrY"] - 2 * pvmesh.point_data["Visc"] * pvmesh.point_data["Edot"] + pvmesh.point_data["Mat"] = material.rbf_interpolate(pvpoints) + pvmesh.point_data["Strn"] = strain._meshVar.rbf_interpolate(pvpoints) + + # Velocity arrows + + v_vectors = np.zeros_like(pvmesh.points) + v_vectors[:, 0:2] = v_soln.rbf_interpolate(pvpoints) + + # Points (swarm) + + with swarm.access(): + plot_points = np.where(strain.data > 0.0001) + strain_data = strain.data.copy() + + points = np.zeros((swarm.data[plot_points].shape[0], 3)) + points[:, 0] = swarm.data[plot_points[0], 0] + points[:, 1] = swarm.data[plot_points[0], 1] + point_cloud = pv.PolyData(points) + point_cloud.point_data["strain"] = strain.data[plot_points] + + pl = pv.Plotter(window_size=(500, 500)) + + # pl.add_arrows(pvmesh.points, v_vectors, mag=0.1, opacity=0.75) + # pl.camera_position = "xy" + + pl.add_mesh( + pvmesh, + cmap="Blues", + edge_color="Grey", + show_edges=True, + # clim=[-1.0,1.0], + scalars="Edotp", + use_transparency=False, + opacity=0.5, + ) + + pl.add_points( + point_cloud, + colormap="Oranges", + scalars="strain", + point_size=10.0, + opacity=0.0, + # clim=[0.0,0.2], + ) + + pl.camera.SetPosition(0.0, 0.0, 3.0) + pl.camera.SetFocalPoint(0.0, 0.0, 0.0) + pl.camera.SetClippingRange(1.0, 8.0) + + pl.show() +# - + +pvmesh.point_data["Strn"].shape + +import matplotlib.pyplot as plt + +plt.scatter(mesh1.data[:, 0], mesh1.data[:, 1], c=pvmesh.point_data["Strn"]) + + +with swarm.access(): + print(strain.data.max()) + +strain_rate_inv2_pl.rbf_interpolate(mesh1.data).max() + + +# ## + +mesh1._search_lengths diff --git a/main/_sources/Notebooks/Examples-Sandbox-VEP/Ex_Sheared_Layer_Test.py b/main/_sources/Notebooks/Examples-Sandbox-VEP/Ex_Sheared_Layer_Test.py new file mode 100644 index 0000000..0650a0e --- /dev/null +++ b/main/_sources/Notebooks/Examples-Sandbox-VEP/Ex_Sheared_Layer_Test.py @@ -0,0 +1,653 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Validate constitutive models +# +# Simple shear with material defined by particle swarm (based on inclusion model), position, pressure, strain rate etc. +# Check the implementation of the Jacobians using various non-linear terms. +# + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import os +os.environ["UW_TIMING_ENABLE"] = "1" + +import petsc4py +import underworld3 as uw +import numpy as np +import sympy + +import pyvista as pv +import vtk + +from underworld3 import timing + +resolution = uw.options.getReal("model_resolution", default=0.033) +mu = uw.options.getInt("mu", default=0.5) +maxsteps = uw.options.getInt("max_steps", default=500) + + + + +# + +# Mesh a 2D pipe with a circular hole + +csize = resolution +csize_circle = resolution * 0.5 +res = csize +cellSize = csize + +width = 3.0 +height = 1.0 +radius = 0.0 + +eta1 = 1000 +eta2 = 1 + +from enum import Enum + +## NOTE: stop using pygmsh, then we can just define boundary labels ourselves and not second guess pygmsh + +class boundaries(Enum): + Bottom = 1 + Right = 3 + Top = 2 + Left = 4 + Inclusion = 5 + All_Boundaries = 1001 + + + +if uw.mpi.rank == 0: + + import gmsh + + gmsh.initialize() + gmsh.model.add("Periodic x") + + xmin, ymin = -width / 2, -height / 2 + xmax, ymax = +width / 2, +height / 2 + + p1 = gmsh.model.geo.add_point(xmin, ymin, 0.0, meshSize=cellSize) + p2 = gmsh.model.geo.add_point(xmax, ymin, 0.0, meshSize=cellSize) + p3 = gmsh.model.geo.add_point(xmin, ymax, 0.0, meshSize=cellSize) + p4 = gmsh.model.geo.add_point(xmax, ymax, 0.0, meshSize=cellSize) + + l1 = gmsh.model.geo.add_line(p1, p2, tag=boundaries["Bottom"].value) + l2 = gmsh.model.geo.add_line(p2, p4, tag=boundaries["Right"].value) + l3 = gmsh.model.geo.add_line(p4, p3, tag=boundaries["Top"].value) + l4 = gmsh.model.geo.add_line(p3, p1, tag=boundaries["Left"].value) + + loops = [] + if radius > 0.0: + p5 = gmsh.model.geo.add_point(0.0, 0.0, 0.0, meshSize=csize_circle) + p6 = gmsh.model.geo.add_point(+radius, 0.0, 0.0, meshSize=csize_circle) + p7 = gmsh.model.geo.add_point(-radius, 0.0, 0.0, meshSize=csize_circle) + + c1 = gmsh.model.geo.add_circle_arc(p6, p5, p7) + c2 = gmsh.model.geo.add_circle_arc(p7, p5, p6) + + cl1 = gmsh.model.geo.add_curve_loop([c1, c2], tag=55) + loops = [cl1] + loops + + cl = gmsh.model.geo.add_curve_loop((l1, l2, l3, l4)) + loops = [cl] + loops + + surface = gmsh.model.geo.add_plane_surface(loops, tag=99999) + + gmsh.model.geo.synchronize() + + # translation = [1, 0, 0, width, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] + # gmsh.model.mesh.setPeriodic( + # 1, [boundaries["Right"]], [boundaries["Left"]], translation + # ) + + # Add Physical groups + + for bd in boundaries: + print(bd.value, flush=True) + print(bd.name, flush=True) + gmsh.model.add_physical_group(1, [bd.value], bd.value) + gmsh.model.set_physical_name(1, bd.value, bd.name) + + if radius > 0.0: + gmsh.model.addPhysicalGroup(1, [c1, c2], 55) + gmsh.model.setPhysicalName(1, 55, "Inclusion") + + gmsh.model.addPhysicalGroup(2, [surface], surface) + gmsh.model.setPhysicalName(2, surface, "Elements") + + # %% + gmsh.model.mesh.generate(2) + gmsh.write("tmp_shear_inclusion.msh") + gmsh.finalize() + +# - + + + + +# + +mesh1 = uw.discretisation.Mesh("tmp_shear_inclusion.msh", + simplex=True, markVertices=True, + useRegions=True, boundaries=boundaries + ) + +## build periodic mesh (mesh1) +# mesh1.view() +# uw.cython.petsc_discretisation.petsc_dm_set_periodicity( +# mesh1.dm, [0.1, 0.0], [-1.5, 0.0], [1.5, 0.0]) + + +# - + + + +# + + +swarm = uw.swarm.Swarm(mesh=mesh1, recycle_rate=5) + +material = uw.swarm.SwarmVariable( + "M", swarm, size=1, + proxy_continuous=True, proxy_degree=2, dtype=int, +) + +strain_p = uw.swarm.SwarmVariable( + "Strain_p", swarm, size=1, + proxy_continuous=True, + proxy_degree=2, varsymbol=r"{\varepsilon_{p}}", dtype=float, +) + +stress_dt = uw.swarm.SwarmVariable(r"Stress_p", swarm, (2,2), vtype=uw.VarType.SYM_TENSOR, varsymbol=r"{\sigma^{*}_{p}}") + +swarm.populate(fill_param=2) + +# + +# Define some functions on the mesh + +import sympy + +# Some useful coordinate stuff + +x, y = mesh1.X + +# relative to the centre of the inclusion +r = sympy.sqrt(x**2 + y**2) +th = sympy.atan2(y, x) + +# need a unit_r_vec equivalent + +inclusion_rvec = mesh1.X +inclusion_unit_rvec = inclusion_rvec / inclusion_rvec.dot(inclusion_rvec) +inclusion_unit_rvec = mesh1.vector.to_matrix(inclusion_unit_rvec) + + +# + +v_soln = uw.discretisation.MeshVariable("U", mesh1, mesh1.dim, degree=2) +p_soln = uw.discretisation.MeshVariable("P", mesh1, 1, degree=1, continuous=True) +work = uw.discretisation.MeshVariable(r"W", mesh1, 1, degree=1, continuous=False) +Stress = uw.discretisation.MeshVariable(r"{\sigma}", mesh1, (2,2), vtype=uw.VarType.SYM_TENSOR, degree=1, + continuous=False, varsymbol=r"{\sigma}") + +vorticity = uw.discretisation.MeshVariable("omega", mesh1, 1, degree=1) +strain_rate_inv2 = uw.discretisation.MeshVariable("eps", mesh1, 1, degree=2) +strain_rate_inv2_p = uw.discretisation.MeshVariable("eps_p", mesh1, 1, degree=2, varsymbol=r"\dot\varepsilon_p") +dev_stress_inv2 = uw.discretisation.MeshVariable("tau", mesh1, 1, degree=2) +yield_stress = uw.discretisation.MeshVariable("tau_y", mesh1, 1, degree=1) + +node_viscosity = uw.discretisation.MeshVariable("eta", mesh1, 1, degree=1) +r_inc = uw.discretisation.MeshVariable("R", mesh1, 1, degree=1) +# - +mesh1.view() + +0/0 + +# + +# Set the initial strain from the mesh + +with mesh1.access(): #strain_rate_inv2_p): + XX = v_soln.coords[:,0] + YY = v_soln.coords[:,1] + mask = (1.0 - (YY * 2)**8) * (1 - (2*XX/3)**6) + # strain_rate_inv2_p.data[:,0] = 2.0 * np.floor(0.033+np.random.random(strain_rate_inv2_p.coords.shape[0])) * mask + +# + + +with swarm.access(material, strain_p), mesh1.access(): + strain_p.data[:] = strain_rate_inv2_p.rbf_interpolate(swarm.particle_coordinates.data) + strain_array = strain_rate_inv2_p.rbf_interpolate(swarm.particle_coordinates.data) +# + + +with swarm.access(strain_p, material), mesh1.access(): + XX = swarm.particle_coordinates.data[:,0] + YY = swarm.particle_coordinates.data[:,1] + mask = (1.0 - (YY * 2)**8) * (1 - (2*XX/3)**6) + material.data[(XX**2 + YY**2 < 0.01), 0] = 1 + strain_p.data[:,0] = 0.0 * np.random.random(swarm.particle_coordinates.data.shape[0]) * mask +# - + + + +# + +# Create Solver object + +stokes = uw.systems.Stokes( + mesh1, + velocityField=v_soln, + pressureField=p_soln, + verbose=True, + solver_name="stokes", +) + +eta1 = 1000 +eta2 = 1 + +viscosity_L = sympy.Piecewise( + (eta2, material.sym[0] > 0.5), + (eta1, True), +) + +# - +stokes.constitutive_model = uw.constitutive_models.ViscoElasticPlasticFlowModel +stokes.constitutive_model.Parameters.bg_viscosity = viscosity_L +# stokes.constitutive_model.Parameters.sigma_star_fn + + +stokes.constitutive_model.viscosity + +stokes.constitutive_model + +stokes.constitutive_model.Parameters.shear_modulus = 1.0 +stokes.constitutive_model.Parameters.dt_elastic = 0.1 + +stokes.constitutive_model + +stokes.stress_deviator_1d + +sigma_projector = uw.systems.Tensor_Projection(mesh1, tensor_Field=Stress, scalar_Field=work ) +sigma_projector.uw_function = stokes.stress_1d + +# + +nodal_strain_rate_inv2 = uw.systems.Projection( + mesh1, strain_rate_inv2, solver_name="edot_II" +) +nodal_strain_rate_inv2.uw_function = stokes.Unknowns.Einv2 +nodal_strain_rate_inv2.smoothing = 1.0e-3 +nodal_strain_rate_inv2.petsc_options.delValue("ksp_monitor") + +nodal_tau_inv2 = uw.systems.Projection(mesh1, dev_stress_inv2, solver_name="stress_II") +nodal_tau_inv2.uw_function = ( + 2 * stokes.constitutive_model.viscosity * stokes.Unknowns.Einv2 +) +nodal_tau_inv2.smoothing = 1.0e-3 +nodal_tau_inv2.petsc_options.delValue("ksp_monitor") + +yield_stress_calc = uw.systems.Projection(mesh1, yield_stress, solver_name="stress_y") +yield_stress_calc.uw_function = 0.0 +yield_stress_calc.smoothing = 1.0e-3 +yield_stress_calc.petsc_options.delValue("ksp_monitor") + +nodal_visc_calc = uw.systems.Projection(mesh1, node_viscosity, solver_name="visc") +nodal_visc_calc.uw_function = stokes.constitutive_model.viscosity +nodal_visc_calc.smoothing = 1.0e-3 +nodal_visc_calc.petsc_options.delValue("ksp_monitor") + + + +# + +# Set solve options here (or remove default values +# stokes.petsc_options.getAll() + +# Constant visc + +stokes.penalty = 1.0 +stokes.bodyforce = ( + -0.00000001 * mesh1.CoordinateSystem.unit_e_1.T +) # vertical force term (non-zero pressure) + +stokes.tolerance = 1.0e-4 + +# stokes.bodyforce -= 1.0e6 * surface_defn_fn * v_soln.sym.dot(inclusion_unit_rvec) * inclusion_unit_rvec + +# Velocity boundary conditions + +if radius > 0.0: + stokes.add_dirichlet_bc((0.0, 0.0), "Inclusion") + +stokes.add_dirichlet_bc((1.0, 0.0), "Top") +stokes.add_dirichlet_bc((-1.0, 0.0), "Bottom") +stokes.add_dirichlet_bc((sympy.oo, 0.0), "Left") +stokes.add_dirichlet_bc((sympy.oo, 0.0), "Right") + +# - + + +mesh1.dm.view() + +stokes._setup_pointwise_functions() +stokes._setup_discretisation(verbose=True) +stokes._setup_solver() + +stokes.solve() + +sigma_projector.solve() + +with mesh1.access(): + print(Stress[0,0].data.max()) + print(Stress[1,1].data.max()) + print(Stress[0,1].data.max()) + + +# + +# Now add yield without pressure dependence + +eps_ref = sympy.sympify(1) +scale = sympy.sympify(25) +C0 = 2500 +Cinf = 500 + +# C = 2 * (y * 2)**16 + 5.0 * sympy.exp(-(strain.sym[0]/0.1)**2) + 0.1 +C = 2 * (y * 2)**2 + (C0-Cinf) * (1 - sympy.tanh((strain_p.sym[0]/eps_ref - 1)*scale) ) / 2 + Cinf + +stokes.constitutive_model.Parameters.yield_stress = C + mu * p_soln.sym[0] +stokes.constitutive_model.Parameters.edot_II_fn = stokes.Unknowns.Einv2 +stokes.constitutive_model.Parameters.min_viscosity = 0.1 +stokes.saddle_preconditioner = 1 / stokes.constitutive_model.viscosity + + +# stokes.solve(zero_init_guess=False, picard=2) + +stokes.constitutive_model +# - + +stokes.constitutive_model.stress_projection() + +stokes.constitutive_model.flux + +0/0 + +with mesh1.access(): + print(p_soln.data.min(), p_soln.data.max()) + +# + +timing.reset() +timing.start() + +stokes.snes.setType("newtontr") +stokes.solve(zero_init_guess=False, picard = -1) + +timing.print_table(display_fraction=1) +# - + + +stokes._u_f1 + +# + + +S = stokes.stress_deviator +nodal_tau_inv2.uw_function = sympy.simplify(sympy.sqrt(((S**2).trace()) / 2)) +nodal_tau_inv2.solve() + +yield_stress_calc.uw_function = stokes.constitutive_model.Parameters.yield_stress +yield_stress_calc.solve() + +nodal_visc_calc.uw_function = sympy.log(stokes.constitutive_model.Parameters.viscosity) +nodal_visc_calc.solve() + +nodal_strain_rate_inv2.uw_function = (sympy.Max(0.0, stokes._Einv2 - + 0.5 * stokes.constitutive_model.Parameters.yield_stress / stokes.constitutive_model.Parameters.bg_viscosity)) +nodal_strain_rate_inv2.solve() + +with mesh1.access(strain_rate_inv2_p): + strain_rate_inv2_p.data[...] = strain_rate_inv2.data.copy() + +nodal_strain_rate_inv2.uw_function = stokes._Einv2 +nodal_strain_rate_inv2.solve() + +# - +mesh0 = uw.meshing.UnstructuredSimplexBox( + minCoords=(-1.5,-0.5), + maxCoords=(+1.5,+0.5), + cellSize=0.033, +) + + +# + +# check it - NOTE - for the periodic mesh, points which have crossed the coordinate sheet are plotted somewhere +# unexpected. This is a limitation we are stuck with for the moment. + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh1) + + pvpoints = pvmesh.points[:, 0:2] + usol = v_soln.rbf_interpolate(pvpoints) + + pvmesh.point_data["P"] = p_soln.rbf_interpolate(pvpoints) + pvmesh.point_data["Edot"] = strain_rate_inv2.rbf_interpolate(pvpoints)**2 + pvmesh.point_data["Edotp"] = strain_rate_inv2_p.rbf_interpolate(pvpoints) + pvmesh.point_data["Strs"] = dev_stress_inv2.rbf_interpolate(pvpoints) + pvmesh.point_data["StrY"] = yield_stress.rbf_interpolate(pvpoints) + pvmesh.point_data["dStrY"] = pvmesh.point_data["Strs"] - pvmesh.point_data["StrY"] + pvmesh.point_data["Visc"] = np.exp(node_viscosity.rbf_interpolate(pvpoints)) + pvmesh.point_data["Mat"] = material.rbf_interpolate(pvpoints) + pvmesh.point_data["Strn"] = strain._meshVar.rbf_interpolate(pvpoints) + + # Velocity arrows + + v_vectors = np.zeros_like(pvmesh.points) + v_vectors[:, 0:2] = v_soln.rbf_interpolate(pvpoints) + + # Points (swarm) + + with swarm.access(): + points = np.zeros((swarm.data.shape[0], 3)) + points[:, 0] = swarm.data[:,0] + points[:, 1] = swarm.data[:,1] + point_cloud = pv.PolyData(points) + point_cloud.point_data["strain"] = strain.data[:,0] + + pl = pv.Plotter(window_size=(500, 500)) + + # pl.add_arrows(pvmesh.points, v_vectors, mag=0.1, opacity=0.75) + # pl.camera_position = "xy" + + pl.add_mesh( + pvmesh, + cmap="Blues", + edge_color="Grey", + show_edges=True, + # clim=[0.0,1.0], + scalars="P", + use_transparency=False, + opacity=0.5, + ) + + # pl.add_points(point_cloud, colormap="coolwarm", scalars="strain", point_size=10.0, opacity=0.5) + + pl.camera.SetPosition(0.0, 0.0, 3.0) + pl.camera.SetFocalPoint(0.0, 0.0, 0.0) + pl.camera.SetClippingRange(1.0, 8.0) + + pl.show() +# + +# Now add elasticity +# - +stokes.constitutive_model.Parameters.yield_stress.subs(((strain.sym[0],0.25), (y,0.0))) + +stokes.constitutive_model.Parameters.viscosity + + +def return_points_to_domain(coords): + new_coords = coords.copy() + new_coords[:,0] = (coords[:,0] + 1.5)%3 - 1.5 + return new_coords + + +ts = 0 + +# + +delta_t = stokes.estimate_dt() + +expt_name = f"output/shear_band_sw_nonp_{mu}" + +for step in range(0, 10): + + stokes.solve(zero_init_guess=False) + + nodal_strain_rate_inv2.uw_function = (sympy.Max(0.0, stokes._Einv2 - + 0.5 * stokes.constitutive_model.Parameters.yield_stress / stokes.constitutive_model.Parameters.bg_viscosity)) + nodal_strain_rate_inv2.solve() + + with mesh1.access(strain_rate_inv2_p): + strain_rate_inv2_p.data[...] = strain_rate_inv2.data.copy() + + nodal_strain_rate_inv2.uw_function = stokes._Einv2 + nodal_strain_rate_inv2.solve() + + with swarm.access(strain), mesh1.access(): + XX = swarm.particle_coordinates.data[:,0] + YY = swarm.particle_coordinates.data[:,1] + mask = (2*XX/3)**4 # * 1.0 - (YY * 2)**8 + strain.data[:,0] += delta_t * mask * strain_rate_inv2_p.rbf_interpolate(swarm.data)[:,0] - 0.1 * delta_t + strain_dat = delta_t * mask * strain_rate_inv2_p.rbf_interpolate(swarm.data)[:,0] + print(f"dStrain / dt = {delta_t * (mask * strain_rate_inv2_p.rbf_interpolate(swarm.data)[:,0]).mean()}, {delta_t}") + + mesh1.write_timestep_xdmf(f"{expt_name}", + meshUpdates=False, + meshVars=[p_soln,v_soln,strain_rate_inv2_p], + swarmVars=[strain], + index=ts) + + swarm.save(f"{expt_name}.swarm.{ts}.h5") + strain.save(f"{expt_name}.strain.{ts}.h5") + + # Update the swarm locations + swarm.advection(v_soln.sym, delta_t=delta_t, + restore_points_to_domain_func=None) + + if uw.mpi.rank == 0: + print("Timestep {}, dt {}".format(step, delta_t)) + + ts += 1 + +# - + + + + +# + +nodal_visc_calc.uw_function = sympy.log(stokes.constitutive_model.Parameters.viscosity) +nodal_visc_calc.solve() + +yield_stress_calc.uw_function = stokes.constitutive_model.Parameters.yield_stress +yield_stress_calc.solve() + +nodal_tau_inv2.uw_function = 2 * stokes.constitutive_model.Parameters.viscosity * stokes._Einv2 +nodal_tau_inv2.solve() + +# + +# check it - NOTE - for the periodic mesh, points which have crossed the coordinate sheet are plotted somewhere +# unexpected. This is a limitation we are stuck with for the moment. + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh1) + + pvpoints = pvmesh.points[:, 0:2] + usol = v_soln.rbf_interpolate(pvpoints) + + pvmesh.point_data["P"] = p_soln.rbf_interpolate(pvpoints) + pvmesh.point_data["Edot"] = strain_rate_inv2.rbf_interpolate(pvpoints) + pvmesh.point_data["Visc"] = np.exp(node_viscosity.rbf_interpolate(pvpoints)) + pvmesh.point_data["Edotp"] = strain_rate_inv2_p.rbf_interpolate(pvpoints) + pvmesh.point_data["Strs"] = dev_stress_inv2.rbf_interpolate(pvpoints) + pvmesh.point_data["StrY"] = yield_stress.rbf_interpolate(pvpoints) + pvmesh.point_data["dStrY"] = pvmesh.point_data["StrY"] - 2 * pvmesh.point_data["Visc"] * pvmesh.point_data["Edot"] + pvmesh.point_data["Mat"] = material.rbf_interpolate(pvpoints) + pvmesh.point_data["Strn"] = strain._meshVar.rbf_interpolate(pvpoints) + + # Velocity arrows + + v_vectors = np.zeros_like(pvmesh.points) + v_vectors[:, 0:2] = v_soln.rbf_interpolate(pvpoints) + + # Points (swarm) + + with swarm.access(): + plot_points = np.where(strain.data > 0.0001) + strain_data = strain.data.copy() + + points = np.zeros((swarm.data[plot_points].shape[0], 3)) + points[:, 0] = swarm.data[plot_points[0],0] + points[:, 1] = swarm.data[plot_points[0],1] + point_cloud = pv.PolyData(points) + point_cloud.point_data["strain"] = strain.data[plot_points] + + pl = pv.Plotter(window_size=(500, 500)) + + # pl.add_arrows(pvmesh.points, v_vectors, mag=0.1, opacity=0.75) + # pl.camera_position = "xy" + + pl.add_mesh( + pvmesh, + cmap="Blues", + edge_color="Grey", + show_edges=True, + # clim=[-1.0,1.0], + scalars="Edotp", + use_transparency=False, + opacity=0.5, + ) + + + pl.add_points(point_cloud, + colormap="Oranges", scalars="strain", + point_size=10.0, + opacity=0.0, + # clim=[0.0,0.2], + ) + + pl.camera.SetPosition(0.0, 0.0, 3.0) + pl.camera.SetFocalPoint(0.0, 0.0, 0.0) + pl.camera.SetClippingRange(1.0, 8.0) + + pl.show() +# - + +strain_dat.max() + + + +with swarm.access(): + print(strain.data.max()) + +strain_rate_inv2_p.rbf_interpolate(mesh1.data).max() + + + +# ## + +mesh1._search_lengths + + diff --git a/main/_sources/Notebooks/Examples-Sandbox-VEP/Ex_Sheared_Layer_Visualisation.py b/main/_sources/Notebooks/Examples-Sandbox-VEP/Ex_Sheared_Layer_Visualisation.py new file mode 100644 index 0000000..6b5dc02 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Sandbox-VEP/Ex_Sheared_Layer_Visualisation.py @@ -0,0 +1,169 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Validate constitutive models +# +# Simple shear with material defined by particle swarm (based on inclusion model), position, pressure, strain rate etc. Check the implmentation of the Jacobians using various non-linear terms. +# + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import petsc4py +import underworld3 as uw +import numpy as np + +import pyvista as pv +import vtk + +pv.global_theme.background = "white" +pv.global_theme.window_size = [1250, 500] +pv.global_theme.anti_aliasing = "ssaa" +pv.global_theme.jupyter_backend = "trame" +pv.global_theme.smooth_shading = True +pv.global_theme.camera["viewup"] = [0.0, 1.0, 0.0] +pv.global_theme.camera["position"] = [0.0, 0.0, 20.0] + + +# + +step = 1 #50 + +# basename = "/Users/lmoresi/+Simulations/ShearTest/ShearTestHP_InclusionMu05/shear_band_sw_nonp_0.5" +basename = "output/shear_band_sw_nonp_0.5" + + +# + language="sh" +# ls -trl "/Users/lmoresi/+Simulations/ShearTest/ShearTestHP_InclusionMu05/" | tail + +# + +# # ls /Users/lmoresi/+Simulations/ShearTest/ShearMu05_res0.01 + +# + +# Simplified (not periodic) mesh that covers the +# same area as the original (periodic) mesh + +mesh1 = uw.meshing.UnstructuredSimplexBox( + minCoords=(-1.5,-0.5), + maxCoords=(+1.5,+0.5), + cellSize=0.02, +) + +swarm = uw.swarm.Swarm(mesh=mesh1) + + +# + +v_soln = uw.discretisation.MeshVariable("U", mesh1, mesh1.dim, degree=2) +p_soln = uw.discretisation.MeshVariable("P", mesh1, 1, degree=1, continuous=True) + +strain_rate_inv2 = uw.discretisation.MeshVariable("eps", mesh1, 1, degree=1) +strain_rate_inv2_p = uw.discretisation.MeshVariable("eps_p", mesh1, 1, degree=1, varsymbol=r"\dot\varepsilon_p") +strain_p = uw.discretisation.MeshVariable("p_strain", mesh1, 1, degree=2, varsymbol=r"varepsilon_p") + +strain = uw.swarm.SwarmVariable( + "Strain", swarm, size=1, + proxy_degree=1, proxy_continuous=False, + varsymbol=r"\varepsilon", dtype=float, +) + +# dev_stress_inv2 = uw.discretisation.MeshVariable("tau", mesh1, 1, degree=2) +# yield_stress = uw.discretisation.MeshVariable("tau_y", mesh1, 1, degree=1) + +swarm.load(f"{basename}.swarm.{step}.h5") +strain.load(f"{basename}.strain.{step}.h5", swarmFilename=f"{basename}.swarm.{step}.h5") + +# - + + +v_soln.read_from_vertex_checkpoint(f"{basename}.U.{step}.h5", "U") +p_soln.read_from_vertex_checkpoint(f"{basename}.P.{step}.h5", "P") +strain_rate_inv2_p.read_from_vertex_checkpoint(f"{basename}.eps_p.{step}.h5", "eps_p") +strain_p.read_from_vertex_checkpoint(f"{basename}.proxy.Strain.{step}.h5", "proxy_Strain") + +# + +mesh1.vtk("tmp_shear_inclusion.vtk") +pvmesh = pv.read("tmp_shear_inclusion.vtk") + +pvpoints = pvmesh.points[:, 0:2] +usol = v_soln.rbf_interpolate(pvpoints) + + +pvmesh.point_data["P"] = p_soln.rbf_interpolate(pvpoints) +pvmesh.point_data["Edotp"] = strain_rate_inv2_p.rbf_interpolate(pvpoints) +pvmesh.point_data["Strn"] = strain_p.rbf_interpolate(pvpoints) +pvmesh.point_data["Strn2"] = strain._meshVar.rbf_interpolate(pvpoints) + +# Velocity arrows + +v_vectors = np.zeros_like(pvmesh.points) +v_vectors[:, 0:2] = v_soln.rbf_interpolate(pvpoints) + +# Points (swarm) + +with swarm.access(): + plot_points = np.where(strain.data > 0.0) + strain_data = strain.data.copy() + + points = np.zeros((swarm.data[plot_points].shape[0], 3)) + points[:, 0] = swarm.data[plot_points[0],0] + points[:, 1] = swarm.data[plot_points[0],1] + point_cloud = pv.PolyData(points) + point_cloud.point_data["strain"] = strain.data[plot_points] + +pl = pv.Plotter(window_size=(500, 500)) + +# pl.add_arrows(pvmesh.points, v_vectors, mag=0.05, opacity=0.25) +# pl.camera_position = "xy" + + +pl.add_mesh( + pvmesh, + cmap="Blues", + edge_color="Grey", + show_edges=False, + # clim=[0.1,0.5], + scalars="Edotp", + use_transparency=False, + opacity=0.75, +) + +pl.add_points(point_cloud, colormap="Oranges", + scalars="strain", + # clim=[0.0,0.001], + point_size=5.0, + opacity=0.5) + +pl.camera.SetPosition(0.0, 0.0, 3.0) +pl.camera.SetFocalPoint(0.0, 0.0, 0.0) +pl.camera.SetClippingRange(1.0, 8.0) + +# pl.camera.SetPosition(0.75, 0.2, 1.5) +# pl.camera.SetFocalPoint(0.75, 0.2, 0.0) + + +pl.screenshot( + filename=f"{basename}.{step}.png", + window_size=(2560, 1280), + return_img=False, + ) + + +pl.show() +# - + + + + + diff --git a/main/_sources/Notebooks/Examples-Sandbox-VEP/Ex_VP-CM_slabSubduction.py b/main/_sources/Notebooks/Examples-Sandbox-VEP/Ex_VP-CM_slabSubduction.py new file mode 100644 index 0000000..65ec081 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Sandbox-VEP/Ex_VP-CM_slabSubduction.py @@ -0,0 +1,459 @@ +#!/usr/bin/env python +# coding: utf-8 +# %% [markdown] +# # Slab subduction +# +# +# #### [From Dan Sandiford](https://github.com/dansand/uw3_models/blob/main/slabsubduction.ipynb) +# +# +# +# UW2 example ported to UW3 + +# %% +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# %% +import numpy as np +import os +import math +from petsc4py import PETSc +import underworld3 as uw + + +from underworld3.utilities import generateXdmf + + +from sympy import Piecewise, ceiling, Abs, Min, sqrt, eye, Matrix, Max + + +# %% +expt_name = "output/slabSubduction/" + +if uw.mpi.rank == 0: + ### delete previous model run + if os.path.exists(expt_name): + for i in os.listdir(expt_name): + os.remove(expt_name + i) + + ### create folder if not run before + if not os.path.exists(expt_name): + os.makedirs(expt_name) + + +# %% +### For visualisation +render = True + + +# %% + + +options = PETSc.Options() + +options["snes_converged_reason"] = None +options["snes_monitor_short"] = None + + +# %% +n_els = 30 +dim = 2 +boxLength = 4.0 +boxHeight = 1.0 +ppcell = 5 + +# %% [markdown] +# ### Create mesh and mesh vars + +# %% +mesh = uw.meshing.StructuredQuadBox( + elementRes=(4 * n_els, n_els), + minCoords=(0.0,) * dim, + maxCoords=(boxLength, boxHeight), +) + + +v = uw.discretisation.MeshVariable("V", mesh, mesh.dim, degree=2) +p = uw.discretisation.MeshVariable("P", mesh, 1, degree=1) + +strain_rate_inv2 = uw.discretisation.MeshVariable("SR", mesh, 1, degree=1) +node_viscosity = uw.discretisation.MeshVariable("Viscosity", mesh, 1, degree=1) +# materialField = uw.discretisation.MeshVariable("Material", mesh, 1, degree=1) + +stokes = uw.systems.Stokes(mesh, velocityField=v, pressureField=p) +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel + + +# %% [markdown] +# ### Create swarm and swarm vars +# - 'swarm.add_variable' is a traditional swarm, can't be used to map material properties. Can be used for sympy operations, similar to mesh vars. +# - 'uw.swarm.IndexSwarmVariable', creates a mask for each material and can be used to map material properties. Can't be used for sympy operations. +# + +# %% +swarm = uw.swarm.Swarm(mesh) + +# %% +## # Add index swarm variable for material +material = uw.swarm.IndexSwarmVariable("M", swarm, indices=5) + +# %% + + +swarm.populate(3) + +# Add some randomness to the particle distribution +import numpy as np + +np.random.seed(0) + +with swarm.access(swarm.particle_coordinates): + factor = 0.5 * boxLength / n_els / ppcell + swarm.particle_coordinates.data[:] += factor * np.random.rand( + *swarm.particle_coordinates.data.shape + ) + + +# %% [markdown] +# #### Project fields to mesh vars +# Useful for visualising stuff on the mesh (Viscosity, material, strain rate etc) and saving to a grouped xdmf file + + +# %% +# material.info() +# """ +# you have 5 materials +# if you want to have material variable rheologies, density +# """ +# phi 1 = material.piecewise([m1_visc,2,3,4,5]) +# phi_2 = material.piecewise([m1_rho, m2_rho, m3_rho]) + +# %% +stokes.constitutive_model.Parameters.shear_viscosity_0 + +# %% +nodal_strain_rate_inv2 = uw.systems.Projection(mesh, strain_rate_inv2) +nodal_strain_rate_inv2.uw_function = stokes.Unknowns.Einv2 +# nodal_strain_rate_inv2.smoothing = 1.0e-3 +nodal_strain_rate_inv2.petsc_options.delValue("ksp_monitor") + +nodal_visc_calc = uw.systems.Projection(mesh, node_viscosity) +nodal_visc_calc.uw_function = stokes.constitutive_model.Parameters.shear_viscosity_0 +# nodal_visc_calc.smoothing = 1.0e-3 +nodal_visc_calc.petsc_options.delValue("ksp_monitor") + +# meshMat = uw.systems.Projection(mesh, materialField) +# meshMat.uw_function = material.sym +# # meshMat.smoothing = 1.0e-3 +# meshMat.petsc_options.delValue("ksp_monitor") + + +def updateFields(): + ### update strain rate + nodal_strain_rate_inv2.uw_function = stokes.Unknowns.Einv2 + nodal_strain_rate_inv2.solve() + + ### update viscosity + nodal_visc_calc.uw_function = stokes.constitutive_model.Parameters.shear_viscosity_0 + nodal_visc_calc.solve(_force_setup=True) + + # ### update material field from swarm + # meshMat.uw_function = material.sym + # meshMat.solve(_force_setup=True) + + +# %% [markdown] +# ## Setup the material distribution + + +# %% +import matplotlib.path as mpltPath + +### initialise the 'material' data to represent two different materials. +upperMantleIndex = 0 +lowerMantleIndex = 1 +upperSlabIndex = 2 +lowerSlabIndex = 3 +coreSlabIndex = 4 + +### Initial material layout has a flat lying slab with at 15\degree perturbation +lowerMantleY = 0.4 +slabLowerShape = np.array( + [ + (1.2, 0.925), + (3.25, 0.925), + (3.20, 0.900), + (1.2, 0.900), + (1.02, 0.825), + (1.02, 0.850), + ] +) +slabCoreShape = np.array( + [ + (1.2, 0.975), + (3.35, 0.975), + (3.25, 0.925), + (1.2, 0.925), + (1.02, 0.850), + (1.02, 0.900), + ] +) +slabUpperShape = np.array( + [ + (1.2, 1.000), + (3.40, 1.000), + (3.35, 0.975), + (1.2, 0.975), + (1.02, 0.900), + (1.02, 0.925), + ] +) + + +# %% +slabLower = mpltPath.Path(slabLowerShape) +slabCore = mpltPath.Path(slabCoreShape) +slabUpper = mpltPath.Path(slabUpperShape) + + +# %% [markdown] +# ### Update the material variable of the swarm + +# %% +with swarm.access(swarm.particle_coordinates, material): + ### for the symbolic mapping of material properties + material.data[:] = upperMantleIndex + material.data[ + swarm.particle_coordinates.data[:, 1] < lowerMantleY + ] = lowerMantleIndex + material.data[ + slabLower.contains_points(swarm.particle_coordinates.data[:]) + ] = lowerSlabIndex + material.data[ + slabCore.contains_points(swarm.particle_coordinates.data[:]) + ] = coreSlabIndex + material.data[ + slabUpper.contains_points(swarm.particle_coordinates.data[:]) + ] = upperSlabIndex + + +# %% +def plot_mat(): + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh) + + points = vis.swarm_to_pv_cloud(swarm) + point_cloud = pv.PolyData(points) + with swarm.access(): + point_cloud.point_data["M"] = material.data.copy() + + pl = pv.Plotter(window_size=(1000, 750)) + + pl.add_mesh(pvmesh, "Black", "wireframe") + + # pl.add_points(point_cloud, color="Black", + # render_points_as_spheres=False, + # point_size=2.5, opacity=0.75) + + pl.add_mesh( + point_cloud, + cmap="coolwarm", + edge_color="Black", + show_edges=False, + scalars="M", + use_transparency=False, + opacity=0.95, + ) + + pl.show(cpos="xy") + + +if render == True: + plot_mat() + + +# %% [markdown] +# ### Function to save output of model +# Saves both the mesh vars and swarm vars + + +# %% +def saveData(step, outputPath): + mesh.petsc_save_checkpoint( + meshVars=[v, p, strain_rate_inv2, node_viscosity], + index=step, + outputPath=outputPath, + ) + + swarm.petsc_save_checkpoint(swarmName="swarm", index=step, outputPath=outputPath) + + +# %% [markdown] +# #### Density + +# %% +mantleDensity = 0.5 +slabDensity = 1.0 + +density_fn = material.createMask( + [mantleDensity, mantleDensity, slabDensity, slabDensity, slabDensity] +) + + +stokes.bodyforce = Matrix([0, -1 * density_fn]) + + +# %% [markdown] +# ### Boundary conditions +# +# Free slip by only constraining one component of velocity + +# %% +# free slip +stokes.add_dirichlet_bc( + (0.0, 0.0), "Left", (0) +) # left/right: function, boundaries, components +stokes.add_dirichlet_bc((0.0, 0.0), "Right", (0)) + +stokes.add_dirichlet_bc((0.0, 0.0), "Top", (1)) +stokes.add_dirichlet_bc( + (0.0, 0.0), "Bottom", (1) +) # top/bottom: function, boundaries, components + +# %% [markdown] +# ###### initial first guess of constant viscosity + +# %% +if uw.mpi.size == 1: + stokes.petsc_options["pc_type"] = "lu" + +stokes.petsc_options["snes_max_it"] = 500 + +stokes.tolerance = 1e-6 + +# %% +### initial linear solve +# stokes.constitutive_model.Parameters.viscosity = 1.0 +# stokes.saddle_preconditioner = 1 / stokes.constitutive_model.Parameters.viscosity + +stokes.constitutive_model.Parameters.shear_viscosity_0 = 1.0 +stokes.saddle_preconditioner = 1 / stokes.constitutive_model.Parameters.shear_viscosity_0 +stokes.solve(zero_init_guess=True) + + +# %% [markdown] +# #### add in NL rheology for solve loop +# +# CM-VP mockup + +# %% +### viscosity from UW2 example +upperMantleViscosity = 1.0 +lowerMantleViscosity = 100.0 +slabViscosity = 500.0 +coreViscosity = 500.0 + + +strainRate_2ndInvariant = stokes.Unknowns.Einv2 + + +# %% +import sympy + +# %% +### set background/initial viscoisity +shear_viscosity_0 = [ + upperMantleViscosity, + lowerMantleViscosity, + slabViscosity, + slabViscosity, + coreViscosity, +] + +yield_stress = [0, 0, 0.06, 0.06, 0] + +# %% +# stokes.constitutive_model = uw.constitutive_models.ViscoPlasticFlowModel + +# %% +# stokes.constitutive_model.Parameters.shear_viscosity_0 = shear_viscosity_0 +# # stokes.constitutive_model.Parameters.viscosity + +# %% +# stokes.constitutive_model.Parameters.materialIndex = material +# # stokes.constitutive_model.Parameters.viscosity + +# %% +# stokes.constitutive_model.Parameters.yield_stress = np.array(yield_stress) +# # stokes.constitutive_model.Parameters.viscosity + +# %% +# stokes.constitutive_model.Parameters.strainrate_inv_II = stokes.Unknowns.Einv2 +# # stokes.constitutive_model.Parameters.viscosity + +# %% +# stokes.constitutive_model.Parameters.strainrate_inv_II_min = 1.0e-18 +# # stokes.constitutive_model.Parameters.viscosity + +# %% +# stokes.constitutive_model.Parameters.yield_stress_min = np.array(yield_stress)*0.001 +# stokes.constitutive_model.Parameters.viscosity + +# %% +# stokes.constitutive_model.Parameters.yield_stress = np.array(yield_stress)*mesh.X[1] +# stokes.constitutive_model.Parameters.viscosity + +# %% +stokes.constitutive_model.Parameters.averaging_method = "min" +# stokes.constitutive_model.Parameters.viscosity + +# %% [markdown] +# ### Main loop +# Stokes solve loop + +# %% +step = 0 +max_steps = 1 #50 +time = 0 + + +# timing setup +# viewer.getTimestep() +# viewer.setTimestep(1) + + +while step < max_steps: + print(f"\nstep: {step}, time: {time}") + + # viz for parallel case - write the hdf5s/xdmfs + if step % 10 == 0: + if uw.mpi.rank == 0: + print(f"\nSave data: ") + + ### updates projection of fields to the mesh + updateFields() + + ### saves the mesh and swarm + saveData(step, expt_name) + + if uw.mpi.rank == 0: + print(f"\nStokes solve: ") + + stokes.solve(zero_init_guess=False) + + ### get the timestep + dt = stokes.estimate_dt() + + ### advect the particles according to the timestep + swarm.advection(V_fn=stokes.u.sym, delta_t=dt, corrector=False) + + step += 1 + + time += dt + +# %% diff --git a/main/_sources/Notebooks/Examples-Sandbox-VEP/Readme.md b/main/_sources/Notebooks/Examples-Sandbox-VEP/Readme.md new file mode 100644 index 0000000..1c4afcf --- /dev/null +++ b/main/_sources/Notebooks/Examples-Sandbox-VEP/Readme.md @@ -0,0 +1,20 @@ +# Visco-elastic-plastic models + +a.k.a. Digital Sandbox + +## Recent solver and visualization updates + + - [x] Ex_Compression_Example.py + - [x] Ex_Shear_Band_Notch_Benchmark.py + - [x] Ex_Shear_Band_Plasticity_PS.py + - [ ] Ex_Shear_Band_Plasticity_SS.py + - [ ] Not working properly + - [ ] Ex_Sheared_Layer_Elastic.py + - [ ] need to update swarm.Lagrangian_Updater + - [ ] Ex_Sheared_Layer_Test.py + - [ ] NameError: name 'strain' is not defined + - [ ] Ex_Sheared_Layer_Visualisation.py + - [ ] Input files not found + - [x] Ex_VP-CM_slabSubduction.py + - [ ] Theory_VE_NavierStokes.py + - [ ] NameError: name 'tauY' is not defined diff --git a/main/_sources/Notebooks/Examples-Sandbox-VEP/Theory_VE_NavierStokes.py b/main/_sources/Notebooks/Examples-Sandbox-VEP/Theory_VE_NavierStokes.py new file mode 100644 index 0000000..ba680f8 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Sandbox-VEP/Theory_VE_NavierStokes.py @@ -0,0 +1,722 @@ +# --- +# jupyter: +# jupytext: +# cell_metadata_json: true +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# + \frac [markdown] {"incorrectly_encoded_metadata": "{1}{12} \\mathbf{\\tau^{**}} \\right]"} +# # Viscoelastic Navier-Stokes equation +# +# Here we outline how we combine the numerical NS scheme and the numerical Visco-elastic scheme +# +# ## Navier-Stokes equation +# +# $$ +# \rho\frac{D{\mathbf{u}}}{D t} + \nabla \cdot \boldsymbol{\tau} - \nabla \mathbf{p} = \mathbf{\rho g \hat{\mathbf{z}}} +# $$ +# +# The viscous constitutive law connects the stress to gradients of $\mathbf{v}$ as follows: +# +# $$ +# \boldsymbol{\tau} = \eta ( \nabla \mathbf{u} + (\nabla \mathbf{u})^T ) +# $$ +# +# We next write a discrete problem in terms of corresponding variables defined on a mesh. +# +# $$ +# \rho\frac{\mathbf{u}_{[1]} - \mathbf{u}^*}{\Delta t} = \rho g - \nabla \cdot \mathbf{\tau} + \nabla p +# $$ +# +# where $\mathbf{u}^*$ is the value of $\mathbf{u}$ evaluated at upstream point at a time $t - \delta t$. +# Numerically, this is the value on a particle at the previous timestep. This approximation is the forward Euler integration in time for velocity because $\tau$ is defined in terms of the unknowns. $\mathbf{u}_{[1]}$ denotes that solution uses the 1st order Adams-Moulton scheme and higher order updates are well known: +# +# $$ +# \rho\frac{\mathbf{u}_{[2]} - \mathbf{u}^*}{\Delta t} =\rho g - \nabla \cdot \left[ +# \frac{1}{2} \boldsymbol{\tau} + +# \frac{1}{2} \boldsymbol{\tau^*} +# \right] +# - \nabla p +# $$ +# +# and +# +# $$ +# \rho\frac{\mathbf{u}_{[3]} - \mathbf{u}^*}{\Delta t} +# = \rho g - \nabla \cdot \left[ \frac{5}{12} \boldsymbol{\tau} +# - \frac{1}{12} \boldsymbol{\tau^{**}} +# \right] - \nabla p +# $$ +# +# Where $\boldsymbol\tau^*$ and $\boldsymbol\tau^{**}$ are the upstream history values at $t - \Delta t$ and $t - 2\Delta t$ respectively. +# + \frac [markdown] {"incorrectly_encoded_metadata": "{1}{12} \\boldsymbol{\\tau^{* *}} \\right]"} +# +# In the Navier-stokes problem, it is common to write $\boldsymbol\tau=\eta \left(\nabla \mathbf u + (\nabla \mathbf u)^T \right)$ and $\boldsymbol\tau^*=\eta \left(\nabla \mathbf u^* + (\nabla \mathbf u^*)^T \right)$ which ignores rotation and shearing of the stress during the interval $\Delta T$. This simplifies the implementation because only the velocity history is required, not the history of the stress tensor. +# +# +# ## Viscoelasticity +# +# In viscoelasticity, the elastic part of the deformation is related to the stress rate. If we approach this problem as a perturbation to the viscous Navier-Stokes equation, we first consider the constitutive behaviour +# +# $$ +# \frac{1}{2\mu}\frac{D{\boldsymbol\tau}}{Dt} + \frac{\boldsymbol\tau}{2\eta} = \dot{\boldsymbol\varepsilon} +# $$ +# +# A first order difference form for ${D \tau}/{D t}$ then gives +# +# $$ +# \frac{\boldsymbol\tau - \boldsymbol\tau^{*}}{2 \Delta t \mu} + \frac{\boldsymbol\tau}{2 \eta} = \dot{\boldsymbol\varepsilon} +# $$ +# +# where $\tau^*$ is the stress history along the characteristics associated with the current computational points. Rearranging to find an expression for the current stress in terms of the strain rate: +# +# $$ +# \boldsymbol\tau = 2 \dot\varepsilon \eta_{\textrm{eff}_{(1)}} + \frac{\eta \boldsymbol\tau^{*}}{\Delta t \mu + \eta} +# $$ +# +# where an 'effective viscosity' is introduced, defined as follows: +# +# $$ +# \eta_{\textrm{eff}_{(1)}} = \frac{\Delta t \eta \mu}{\Delta t \mu + \eta} +# $$ +# +# Substituting this definition of the stress into the forward-Euler form of the Navier-Stokes discretisation then gives +# +# $$ +# \rho\frac{\mathbf{u}_{[1]} - \mathbf{u}^*}{\Delta t} = \rho g - \nabla \cdot \left[ 2 \dot{\boldsymbol\varepsilon}\eta_{\textrm{eff}_{(1)}} + \frac{\eta \boldsymbol\tau^{*}}{\Delta t \mu + \eta} \right] + \nabla p +# $$ +# +# and the 2nd order (Crank-Nicholson) form becomes +# +# $$ +# \rho\frac{\mathbf{u}_{[2]} - \mathbf{u}^*}{\Delta t} = \rho g - \frac{1}{2} \nabla \cdot \left[ 2 \dot\varepsilon \eta_{\textrm{eff}_{(1)}} + \left[\frac{\eta}{\Delta t \mu + \eta} + 1\right]\tau^* \right] + \nabla p +# $$ +# +# If we use $\tau^{**}$ in the estimate for the stress rate, we have +# +# $$ +# \frac{3 \tau - 4 \tau^{*} + \tau^{**}}{4 \Delta t \mu} + \frac{\tau}{2 \eta} = \dot\varepsilon +# $$ +# +# Giving +# +# $$ +# \boldsymbol\tau = 2 \dot{\boldsymbol\varepsilon} \eta_{\textrm{eff}_{(2)}} + \frac{4 \eta \boldsymbol\tau^{*}}{2 \Delta t \mu + 3 \eta} - \frac{\eta \boldsymbol\tau^{**}}{2 \Delta t \mu + 3 \eta} +# $$ +# +# $$ +# \eta_{\textrm{eff}_{(2)}} = \frac{2 \Delta t \eta \mu}{2 \Delta t \mu + 3 \eta} +# $$ +# +# +# $$ +# \frac{\mathbf{u}_{[3]} - \mathbf{u}^*}{\Delta t} = \rho g +# - \nabla \cdot \left[ \frac{5 \dot{\boldsymbol\varepsilon} \eta_{\textrm{eff_(2)}}}{6} + +# \frac{5 \eta \boldsymbol\tau^{*}}{3 \cdot \left(2 \Delta t \mu + 3 \eta\right)} + \frac{2\boldsymbol\tau^{*}}{3} +# - \frac{5 \eta \boldsymbol\tau^{**}}{12 \cdot \left(2 \Delta t \mu + 3 \eta\right)} - \frac{\boldsymbol\tau^{**}}{12} +# \right] + \nabla p +# $$ +# +# +# +# --- +# +# $$ +# \nabla\cdot\left[ \color{blue}{ \boldsymbol{\tau} - p \boldsymbol{I} } \right] = \color{green}{\frac{D \boldsymbol{u}}{Dt}} - \rho g +# $$ +# +# - +# $$ +# \frac{5 \dot\varepsilon \eta_\textrm{eff}}{6} + \frac{5 \eta \tau^{*}}{3 \cdot \left(2 \Delta\,\!t \mu + 3 \eta\right)} - \frac{5 \eta \tau^{**}}{12 \cdot \left(2 \Delta\,\!t \mu + 3 \eta\right)} + \frac{2 \tau^{*}}{3} - \frac{\tau^{**}}{12} +# $$ + +# ## Mock up ... BDF / Adams-Moulton coefficients +# +# dot_f term will need BDf coefficients (https://en.wikipedia.org/wiki/Backward_differentiation_formula) +# +# Flux history for Adams Moulton +# Substitute for present stress in ADM +# +# +# +# +# + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + \frac {"incorrectly_encoded_metadata": "{8}{12} \\mathbf{\\tau^*}"} +import os + + +# + \frac {"incorrectly_encoded_metadata": "{2 \\tau^{*}}{3}"} +os.path.join("", "test") + + +# + + +# Symbolic: sympy + uw3 + +import os + +os.environ["UW_TIMING_ENABLE"] = "1" + +import petsc4py +import underworld3 as uw +import numpy as np +import sympy +import pyvista as pv +import vtk + +from underworld3 import timing + +resolution = uw.options.getReal("model_resolution", default=0.033) +mu = uw.options.getInt("mu", default=0.5) +maxsteps = uw.options.getInt("max_steps", default=500) + + +# + +mesh1 = uw.meshing.UnstructuredSimplexBox( + minCoords=(-1.5, -0.5), + maxCoords=(+1.5, +0.5), + cellSize=resolution, +) + +x, y = mesh1.X + +# + +U = uw.discretisation.MeshVariable( + "U", mesh1, mesh1.dim, vtype=uw.VarType.VECTOR, degree=2 +) +P = uw.discretisation.MeshVariable( + "P", mesh1, 1, vtype=uw.VarType.SCALAR, degree=1, continuous=True +) +T = uw.discretisation.MeshVariable("T", mesh1, 1, vtype=uw.VarType.SCALAR, degree=3) + +# Nodal values of deviatoric stress (symmetric tensor) + +work = uw.discretisation.MeshVariable( + "W", mesh1, 1, vtype=uw.VarType.SCALAR, degree=2, continuous=False +) +St = uw.discretisation.MeshVariable( + r"Stress", + mesh1, + (2, 2), + vtype=uw.VarType.SYM_TENSOR, + degree=2, + continuous=False, + varsymbol=r"{\tau}", +) + +# May need these +Edot_inv_II = uw.discretisation.MeshVariable( + "eps_II", + mesh1, + 1, + vtype=uw.VarType.SCALAR, + degree=2, + varsymbol=r"{|\dot\varepsilon|}", +) +St_inv_II = uw.discretisation.MeshVariable( + "tau_II", mesh1, 1, vtype=uw.VarType.SCALAR, degree=2, varsymbol=r"{|\tau|}" +) + +# + +swarm = uw.swarm.Swarm(mesh=mesh1, recycle_rate=5) + +material = uw.swarm.SwarmVariable( + "M", + swarm, + size=1, + vtype=uw.VarType.SCALAR, + proxy_continuous=True, + proxy_degree=1, + dtype=int, +) + +strain_inv_II = uw.swarm.SwarmVariable( + "Strain", + swarm, + size=1, + vtype=uw.VarType.SCALAR, + proxy_continuous=True, + proxy_degree=2, + varsymbol=r"{|\varepsilon|}", + dtype=float, +) + +stress_star = uw.swarm.SwarmVariable( + r"stress_dt", + swarm, + (2, 2), + vtype=uw.VarType.SYM_TENSOR, + proxy_continuous=True, + proxy_degree=2, + varsymbol=r"{\tau^{*}_{p}}", +) + +stress_star_star = uw.swarm.SwarmVariable( + r"stress_2dt", + swarm, + (2, 2), + vtype=uw.VarType.SYM_TENSOR, + proxy_continuous=True, + proxy_degree=2, + varsymbol=r"{\tau^{**}_{p}}", +) + +swarm.populate(fill_param=2) + +# + +stokes = uw.systems.Stokes( + mesh1, + velocityField=U, + pressureField=P, + verbose=False, + solver_name="stokes", +) + +stokes +# - +uw.systems.Stokes + + +# + +from sympy import UnevaluatedExpr + +st = sympy.UnevaluatedExpr(stokes.constitutive_model.viscosity) * sympy.UnevaluatedExpr( + stokes.strainrate +) +st + +# + +eta_0 = sympy.sympify(10) ** -6 +C_0 = sympy.log(10**6) + + +stokes.constitutive_model = uw.constitutive_models.ViscoElasticPlasticFlowModel +stokes.constitutive_model.Parameters.shear_viscosity_0 = sympy.symbols(r"\eta") +stokes.constitutive_model.Parameters.shear_modulus = sympy.symbols(r"\mu") +stokes.constitutive_model.Parameters.stress_star = stress_star.sym +stokes.constitutive_model.Parameters.dt_elastic = sympy.symbols( + r"\Delta\ t" +) # sympy.sympify(1) / 10 +stokes.constitutive_model.Parameters.strainrate_inv_II = stokes.Unknowns.Einv2 +stokes.constitutive_model.Parameters.strainrate_inv_II_min = 0 +stokes.constitutive_model +# - + +stokes.constitutive_model.Parameters.viscosity + +# + +# Set solve options here (or remove default values +# stokes.petsc_options.getAll() + +# Constant visc + +stokes.penalty = 0 + +stokes.tolerance = 1.0e-4 + +# Velocity boundary conditions + +stokes.add_dirichlet_bc((0.0, 0.0), "Inclusion", (0, 1)) +stokes.add_dirichlet_bc((1.0, 0.0), "Top", (0, 1)) +stokes.add_dirichlet_bc((-1.0, 0.0), "Bottom", (0, 1)) +stokes.add_dirichlet_bc((0.0), "Left", (1)) +stokes.add_dirichlet_bc((0.0), "Right", (1)) + +# - + +stokes._setup_problem_description() + +t0 = St.sym +t1 = stress_star.sym +t0 + t1 + +# + +t2 = t0 + t1 +t2 + +t2_II_sq = (t2**2).trace() / 2 +t2_II = sympy.sqrt(t2_II_sq) + +overshoot = sympy.simplify(sympy.sqrt(t2_II_sq) / tauY) +overshoot +# - + +t2_II_sq + +stokes.constitutive_model.flux(stokes.strainrate) + +# Crank-Nicholson timestep - Jacobians +(stokes.constitutive_model.flux(stokes.strainrate) / 2 + stress_star.sym / 2).diff( + stokes._u.sym[0] +) + +(stokes.stress_deviator_1d / 2 + stress_star.sym_1d / 2)[0] + +# RHS +stokes._u_f0 + +# Jacobian terms +stokes._u_f1[1, 1].diff(stokes._L[1, 1]) + +# + +## Jacobians (e.g. stress rate derivatives with respect to strain rate tensor) + +stokes._uu_G3 + +# + +## And now, second order terms + +stokes.constitutive_model = uw.constitutive_models.ViscoElasticPlasticFlowModel( + mesh1.dim +) +stokes.constitutive_model.Parameters.shear_viscosity_0 = eta_0 * sympy.exp( + -C_0 * T.sym[0] +) +stokes.constitutive_model.Parameters.shear_modulus = 100 +stokes.constitutive_model.Parameters.stress_star = stress_star.sym +stokes.constitutive_model.Parameters.stress_star_star = stress_star_star.sym +stokes.constitutive_model.Parameters.dt_elastic = sympy.sympify(1) / 10 +stokes.constitutive_model.Parameters.strainrate_inv_II = stokes._Einv2 +stokes.constitutive_model.Parameters.strainrate_inv_II_min = 0 + +stokes.saddle_preconditioner = 1 / stokes.constitutive_model.Parameters.viscosity + +stokes.constitutive_model + + +# - + +stokes.stress_deviator_1d[2] + +0 / 0 + +stokes.solve() + + +# + +# Adams / Bashforth & Adams Moulton ... + +s = sympy.Symbol(r"\tau") +s1 = sympy.Symbol(r"\tau^*") +s2 = sympy.Symbol(r"\tau^**") +dt = sympy.Symbol(r"\Delta\,\!t") +mu = sympy.Symbol(r"\mu") +eta = sympy.Symbol(r"\eta") +eta_p = sympy.Symbol(r"\eta_p") +eta_eff = sympy.Symbol(r"\eta_\textrm{eff}") +edot = sympy.Symbol(r"\dot\varepsilon") +tr = sympy.Symbol(r"t_r") + +# Stress history + +# 1st order difference expression for stress rate +sdot1 = (s - s1) / dt + +# 2nd order difference for stress rate +sdot2 = (3 * s - 4 * s1 + s2) / (2 * dt) + + +# + +display(sdot1) +display(sdot1 / (2 * mu) + s / (2 * eta)) # + s / (2 * eta_p)) +print(sympy.latex(sdot1 / (2 * mu) + s / (2 * eta) + s / (2 * eta_p))) +Seq1 = sympy.Equality( + sympy.simplify(sdot1 / (2 * mu) + s / (2 * eta) + 0 * s / (2 * eta_p)), edot +) +display(Seq1) + +# solve this for an expression in terms of the present stress, $\tau$ +a = sympy.simplify(sympy.solve(Seq1, s)[0]).expand() +display(a) + +# b = sympy.simplify(sympy.solve(Seq1, eta_p)[0]).expand() +# display(b) +# - + +S = a.subs(edot, stokes.strainrate).subs(s1, stress_star.sym) +SII = sympy.simplify((S**2).trace()) + +# + +## yielding case + +tauY = sympy.symbols(r"\tau_y") + +# - + + +etaY2 = tauY / (2 * edot + (s1 - tauY) / (mu * dt)) +etaY2 + +sympy.simplify(etaY - etaY2) + +stokes.constitutive_model.Parameters.ve_effective_viscosity + +sympy.simplify(a / 2 + s1 / 2).expand().collect(s1) + +# + +# Identify effective viscosity + +eta_eff_1 = sympy.simplify(eta * mu * dt / (mu * dt + eta)) +display(eta_eff_1) +print(sympy.latex(eta_eff_1)) + +# rename this in the equations +b = a.subs(eta_eff_1, eta_eff) +display(b) +print(sympy.latex(b)) + +## An equivalent form for this is + +c = 2 * eta_eff * edot + (s1 * tr / (dt + tr)) +display(c) + +## Validate that +sympy.simplify(b - c.subs(tr, eta / mu)) + +(b / 2 + s1 / 2) + + +# + +# Now we can try a 2nd order + +display(sdot2) +print(sympy.latex(sdot2)) +display(sdot2 / (2 * mu) + s / (2 * eta)) +print(sympy.latex(sdot2 / (2 * mu) + s / (2 * eta))) +Seq2 = sympy.Equality(sympy.simplify(sdot2 / (2 * mu) + s / (2 * eta)), edot) +display(Seq2) +sympy.simplify(sympy.solve(Seq2, s)[0]) + +# + +eta_eff_2 = sympy.simplify(2 * eta * mu * dt / (2 * mu * dt + 3 * eta)) +display(eta_eff_2) +print(sympy.latex(eta_eff_2)) + + +sympy.simplify(2 * eta * sympy.solve(Seq2, s)[0] / (2 * eta_eff_2)) + +# solve this for an expression in terms of the present stress, $\tau$ +a2 = sympy.simplify(sympy.solve(Seq2, s)[0]).expand() +display(a2) + +# Identify effective viscosity + + +# rename this in the equations +b2 = a2.subs(eta_eff_2, eta_eff) +display(b2) +print(sympy.latex(b2)) + +## And this is what happens in Adams-Moulton 3rd order + +display(5 * b2 / 12 + 2 * s1 / 3 - s2 / 12) +print(sympy.latex(5 * b2 / 12 + 2 * s1 / 3 - s2 / 12)) + + +# - + +a = sympy.simplify(2 * eta * sympy.solve(Seq2, s)[0] / (2 * eta_eff_2)) +tau_2 = a.expand().subs(eta / mu, tr) + +a + + +0 / 0 + + +stokes.constitutive_model.Parameters.yield_stress.subs( + ((strain.sym[0], 0.25), (y, 0.0)) +) + +stokes.constitutive_model.Parameters.viscosity + + +def return_points_to_domain(coords): + new_coords = coords.copy() + new_coords[:, 0] = (coords[:, 0] + 1.5) % 3 - 1.5 + return new_coords + + +ts = 0 + +# + +delta_t = stokes.estimate_dt() + +expt_name = f"output/shear_band_sw_nonp_{mu}" + +for step in range(0, 10): + stokes.solve(zero_init_guess=False) + + nodal_strain_rate_inv2.uw_function = sympy.Max( + 0.0, + stokes._Einv2 + - 0.5 + * stokes.constitutive_model.Parameters.yield_stress + / stokes.constitutive_model.Parameters.bg_viscosity, + ) + nodal_strain_rate_inv2.solve() + + with mesh1.access(strain_rate_inv2_p): + strain_rate_inv2_p.data[...] = strain_rate_inv2.data.copy() + + nodal_strain_rate_inv2.uw_function = stokes._Einv2 + nodal_strain_rate_inv2.solve() + + with swarm.access(strain), mesh1.access(): + XX = swarm.particle_coordinates.data[:, 0] + YY = swarm.particle_coordinates.data[:, 1] + mask = (2 * XX / 3) ** 4 # * 1.0 - (YY * 2)**8 + strain.data[:, 0] += ( + delta_t * mask * strain_rate_inv2_p.rbf_interpolate(swarm.data)[:, 0] + - 0.1 * delta_t + ) + strain_dat = ( + delta_t * mask * strain_rate_inv2_p.rbf_interpolate(swarm.data)[:, 0] + ) + print( + f"dStrain / dt = {delta_t * (mask * strain_rate_inv2_p.rbf_interpolate(swarm.data)[:,0]).mean()}, {delta_t}" + ) + + mesh1.write_timestep_xdmf( + f"{expt_name}", + meshUpdates=False, + meshVars=[p_soln, v_soln, strain_rate_inv2_p], + swarmVars=[strain], + index=ts, + ) + + swarm.save(f"{expt_name}.swarm.{ts}.h5") + strain.save(f"{expt_name}.strain.{ts}.h5") + + # Update the swarm locations + swarm.advection(v_soln.sym, delta_t=delta_t, restore_points_to_domain_func=None) + + if uw.mpi.rank == 0: + print("Timestep {}, dt {}".format(step, delta_t)) + + ts += 1 + + + +# + +nodal_visc_calc.uw_function = sympy.log(stokes.constitutive_model.Parameters.viscosity) +nodal_visc_calc.solve() + +yield_stress_calc.uw_function = stokes.constitutive_model.Parameters.yield_stress +yield_stress_calc.solve() + +nodal_tau_inv2.uw_function = ( + 2 * stokes.constitutive_model.Parameters.viscosity * stokes._Einv2 +) +nodal_tau_inv2.solve() + +# + +# check it - NOTE - for the periodic mesh, points which have crossed the coordinate sheet are plotted somewhere +# unexpected. This is a limitation we are stuck with for the moment. + +if uw.mpi.size == 1: + + import underworld3.visualisation as vis # use tools from here + + mesh1.vtk("tmp_shear_inclusion.vtk") + pvmesh = pv.read("tmp_shear_inclusion.vtk") + + pvpoints = pvmesh.points[:, 0:2] + usol = v_soln.rbf_interpolate(pvpoints) + + pvmesh.point_data["P"] = p_soln.rbf_interpolate(pvpoints) + pvmesh.point_data["Edot"] = strain_rate_inv2.rbf_interpolate(pvpoints) + pvmesh.point_data["Visc"] = np.exp(node_viscosity.rbf_interpolate(pvpoints)) + pvmesh.point_data["Edotp"] = strain_rate_inv2_p.rbf_interpolate(pvpoints) + pvmesh.point_data["Strs"] = dev_stress_inv2.rbf_interpolate(pvpoints) + pvmesh.point_data["StrY"] = yield_stress.rbf_interpolate(pvpoints) + pvmesh.point_data["dStrY"] = ( + pvmesh.point_data["StrY"] + - 2 * pvmesh.point_data["Visc"] * pvmesh.point_data["Edot"] + ) + pvmesh.point_data["Mat"] = material.rbf_interpolate(pvpoints) + pvmesh.point_data["Strn"] = strain._meshVar.rbf_interpolate(pvpoints) + + # Velocity arrows + + v_vectors = np.zeros_like(pvmesh.points) + v_vectors[:, 0:2] = v_soln.rbf_interpolate(pvpoints) + + # Points (swarm) + + with swarm.access(): + plot_points = np.where(strain.data > 0.0001) + strain_data = strain.data.copy() + + points = np.zeros((swarm.data[plot_points].shape[0], 3)) + points[:, 0] = swarm.data[plot_points[0], 0] + points[:, 1] = swarm.data[plot_points[0], 1] + point_cloud = pv.PolyData(points) + point_cloud.point_data["strain"] = strain.data[plot_points] + + pl = pv.Plotter(window_size=(500, 500)) + + # pl.add_arrows(pvmesh.points, v_vectors, mag=0.1, opacity=0.75) + # pl.camera_position = "xy" + + pl.add_mesh( + pvmesh, + cmap="Blues", + edge_color="Grey", + show_edges=True, + # clim=[-1.0,1.0], + scalars="Edotp", + use_transparency=False, + opacity=0.5, + ) + + pl.add_points( + point_cloud, + colormap="Oranges", + scalars="strain", + point_size=10.0, + opacity=0.0, + # clim=[0.0,0.2], + ) + + pl.camera.SetPosition(0.0, 0.0, 3.0) + pl.camera.SetFocalPoint(0.0, 0.0, 0.0) + pl.camera.SetClippingRange(1.0, 8.0) + + pl.show() +# - + +strain_dat.max() + +# + +alph = sympy.symbols(r"\alpha_:10") +alph[9] + + +fn = alph[0] * U[0].sym + alph[1] * U[1].sym +# - + +fn.diff(alph[0]) + +with swarm.access(): + print(strain.data.max()) + +strain_rate_inv2_p.rbf_interpolate(mesh1.data).max() + + +# ## + +mesh1._search_lengths diff --git a/main/_sources/Notebooks/Examples-Sandbox-VEP/output/README.md b/main/_sources/Notebooks/Examples-Sandbox-VEP/output/README.md new file mode 100644 index 0000000..c718869 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Sandbox-VEP/output/README.md @@ -0,0 +1,3 @@ +# Sandbox ViscoElastic/Plastic model outputs + +These files are NOT under version control \ No newline at end of file diff --git a/main/_sources/Notebooks/Examples-StokesFlow/Ex_ChannelFlow_IrregularBase.py b/main/_sources/Notebooks/Examples-StokesFlow/Ex_ChannelFlow_IrregularBase.py new file mode 100644 index 0000000..ce6785e --- /dev/null +++ b/main/_sources/Notebooks/Examples-StokesFlow/Ex_ChannelFlow_IrregularBase.py @@ -0,0 +1,388 @@ +# # 3D channel flow +# +# Potentially applicable to ice-sheet flow models + +# + +import underworld3 as uw +import numpy as np +import sympy + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +import os +import sys + +if uw.mpi.size == 1: + import matplotlib.pyplot as plt + + + +elements = 7 +resolution = 1/elements + +outputPath = f"./output/ChannelFlow3D" +expt_name = f"WigglyBottom_{elements}" + +os.makedirs(outputPath, exist_ok=True) + + + +# + +## This is adapted from terrain mesh example provided +## in the gmsh examples. + +from enum import Enum + +class boundaries(Enum): + Upper = 1 + Lower = 2 + Left = 3 + Right = 4 + Front = 5 + Back = 6 + All_Boundaries = 1001 # Petsc Boundary Label + +import gmsh +import math +import sys + +gmsh.initialize() +gmsh.option.setNumber('General.Verbosity', 1) + +gmsh.model.add("terrain") + +# create the terrain surface from N x N input data points (here simulated using +# a simple function): +N = 200 +coords = [] # x, y, z coordinates of all the points +nodes = [] # tags of corresponding nodes +tris = [] # connectivities (node tags) of triangle elements +lin = [[], [], [], []] # connectivities of boundary line elements + + +def tag(i, j): + return (N + 1) * i + j + 1 + +for i in range(N + 1): + X = float(i) / N + for j in range(N + 1): + Y = float(j) / N + + nodes.append(tag(i, j)) + coords.extend([ + 2 * X, + Y, + 0.05 * math.sin(20 * (X + 0.1 * Y)) * math.cos(sympy.pi * Y) - 0.1 * math.sin(sympy.pi * Y) + ]) + if i > 0 and j > 0: + tris.extend([tag(i - 1, j - 1), tag(i, j - 1), tag(i - 1, j)]) + tris.extend([tag(i, j - 1), tag(i, j), tag(i - 1, j)]) + if (i == 0 or i == N) and j > 0: + lin[3 if i == 0 else 1].extend([tag(i, j - 1), tag(i, j)]) + if (j == 0 or j == N) and i > 0: + lin[0 if j == 0 else 2].extend([tag(i - 1, j), tag(i, j)]) +pnt = [tag(0, 0), tag(N, 0), tag(N, N), tag(0, N)] # corner points element + +# create 4 corner points +gmsh.model.geo.addPoint(0, 0, coords[3 * tag(0, 0) - 1], 1) +gmsh.model.geo.addPoint(2, 0, coords[3 * tag(N, 0) - 1], 2) +gmsh.model.geo.addPoint(2, 1, coords[3 * tag(N, N) - 1], 3) +gmsh.model.geo.addPoint(0, 1, coords[3 * tag(0, N) - 1], 4) +gmsh.model.geo.synchronize() + +# create 4 discrete bounding curves, with their boundary points +for i in range(4): + gmsh.model.addDiscreteEntity(1, i + 1, [i + 1, i + 2 if i < 3 else 1]) + +# create one discrete surface, with its bounding curves +topo = gmsh.model.addDiscreteEntity(2, 1, [1, 2, -3, -4]) + +# add all the nodes on the surface (for simplicity... see below) +gmsh.model.mesh.addNodes(2, 1, nodes, coords) + +# add elements on the 4 points, the 4 curves and the surface +for i in range(4): + # type 15 for point elements: + gmsh.model.mesh.addElementsByType(i + 1, 15, [], [pnt[i]]) + # type 1 for 2-node line elements: + gmsh.model.mesh.addElementsByType(i + 1, 1, [], lin[i]) +# type 2 for 3-node triangle elements: +gmsh.model.mesh.addElementsByType(1, 2, [], tris) + +# reclassify the nodes on the curves and the points (since we put them all on +# the surface before for simplicity) +gmsh.model.mesh.reclassifyNodes() + +# note that for more complicated meshes, e.g. for on input unstructured STL, we +# could use gmsh.model.mesh.classifySurfaces() to automatically create the +# discrete entities and the topology; but we would have to extract the +# boundaries afterwards + +# create a geometry for the discrete curves and surfaces, so that we can remesh +# them + +gmsh.model.mesh.createGeometry() + +# create other CAD entities to form one volume below the terrain surface, and +# one volume on top; beware that only built-in CAD entities can be hybrid, +# i.e. have discrete entities on their boundary: OpenCASCADE does not support +# this feature + +p5 = gmsh.model.geo.addPoint(0, 0, 0.5) +p6 = gmsh.model.geo.addPoint(2, 0, 0.5) +p7 = gmsh.model.geo.addPoint(2, 1, 0.5) +p8 = gmsh.model.geo.addPoint(0, 1, 0.5) + +c5 = gmsh.model.geo.addLine(p5, p6) +c6 = gmsh.model.geo.addLine(p6, p7) +c7 = gmsh.model.geo.addLine(p7, p8) +c8 = gmsh.model.geo.addLine(p8, p5) + +c14 = gmsh.model.geo.addLine(1, p5) +c15 = gmsh.model.geo.addLine(2, p6) +c16 = gmsh.model.geo.addLine(3, p7) +c17 = gmsh.model.geo.addLine(4, p8) + +# bottom and top +# ll1 = gmsh.model.geo.addCurveLoop([c1, c2, c3, c4]) +# s1 = gmsh.model.geo.addPlaneSurface([ll1]) + +ll2 = gmsh.model.geo.addCurveLoop([c5, c6, c7, c8]) +s2 = gmsh.model.geo.addPlaneSurface([ll2]) + +# upper +ll7 = gmsh.model.geo.addCurveLoop([c5, -c15, -1, c14]) +s7 = gmsh.model.geo.addPlaneSurface([ll7]) +ll8 = gmsh.model.geo.addCurveLoop([c6, -c16, -2, c15]) +s8 = gmsh.model.geo.addPlaneSurface([ll8]) +ll9 = gmsh.model.geo.addCurveLoop([c7, -c17, 3, c16]) +s9 = gmsh.model.geo.addPlaneSurface([ll9]) +ll10 = gmsh.model.geo.addCurveLoop([c8, -c14, 4, c17]) +s10 = gmsh.model.geo.addPlaneSurface([ll10]) + +sl2 = gmsh.model.geo.addSurfaceLoop([s2, s7, s8, s9, s10, 1]) +v2 = gmsh.model.geo.addVolume([sl2]) + +gmsh.model.geo.synchronize() + +gmsh.model.addPhysicalGroup(2, [s2], boundaries.Upper.value, name=boundaries.Upper.name,) +gmsh.model.addPhysicalGroup(2, [s7], boundaries.Front.value, name=boundaries.Front.name,) +gmsh.model.addPhysicalGroup(2, [s8], boundaries.Right.value, name=boundaries.Right.name,) +gmsh.model.addPhysicalGroup(2, [s9], boundaries.Back.value, name=boundaries.Back.name,) +gmsh.model.addPhysicalGroup(2, [s10], boundaries.Left.value, name=boundaries.Left.name,) + +gmsh.model.addPhysicalGroup(2, [topo], boundaries.Lower.value, name=boundaries.Lower.name,) + +gmsh.model.addPhysicalGroup(3, [v2], 666666, "Elements") + +gmsh.option.setNumber('Mesh.MeshSizeMin', resolution) +gmsh.option.setNumber('Mesh.MeshSizeMax', resolution) +gmsh.model.mesh.generate(3) + +gmsh.write('.meshes/tmp_terrain.msh') + +# gmsh.fltk.run() + +gmsh.finalize() + + +# + +terrain_mesh = uw.discretisation.Mesh( + ".meshes/tmp_terrain.msh", + degree=1, + qdegree=3, + useMultipleTags=True, + useRegions=True, + markVertices=True, + boundaries=boundaries, + coordinate_system_type=None, + refinement=1, + refinement_callback=None, + return_coords_to_bounds=None, + ) + +x,y,z = terrain_mesh.X + +# + +# l = terrain_mesh.dm.getLabel("Lower") +# i = l.getStratumSize(2) +# ii = uw.utilities.gather_data(np.array([float(i)])) + +# if uw.mpi.rank == 0: +# print(f"Nodes in LOWER by rank: {ii.astype(int)}", flush=True) + +# uw.mpi.barrier() +# - +terrain_mesh.view() + + + + + +0/0 + +terrain_mesh.dm.view() + +# + +n_vect = uw.discretisation.MeshVariable("Gamma", terrain_mesh, vtype=uw.VarType.VECTOR, + degree=2, varsymbol="{\Gamma_N}") + +projection = uw.systems.Vector_Projection(terrain_mesh, n_vect) +projection.uw_function = sympy.Matrix([[0,0,0]]) + +GammaNorm = 1 / sympy.sqrt(terrain_mesh.Gamma.dot(terrain_mesh.Gamma)) + +projection.add_natural_bc(terrain_mesh.Gamma * GammaNorm, "Lower") +projection.smoothing = 1.0e-6 +projection.solve(verbose=False) + +# Ensure n_vect are unit vectors +with terrain_mesh.access(n_vect): + n_vect.data[:,:] /= np.sqrt(n_vect.data[:,0]**2 + n_vect.data[:,1]**2 + n_vect.data[:,2]**2).reshape(-1,1) + +# - + +with terrain_mesh.access(n_vect): + print(n_vect.data.max(), flush=True) + + + +# + +stokes = uw.systems.Stokes(terrain_mesh, solver_name="stokes_terrain") +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.shear_viscosity_0 = 1 +stokes.penalty = 1.0 + +v = stokes.Unknowns.u +p = stokes.Unknowns.p + +stokes.add_essential_bc( [sympy.oo, 0.0, 0.0 ], "Left") +stokes.add_essential_bc( [sympy.oo, 0.0, 0.0 ], "Right") +stokes.add_essential_bc( [0.0, 0.0, 0.0 ], "Front") +stokes.add_essential_bc( [0.0, 0.0, 0.0 ], "Back") +stokes.add_essential_bc( [sympy.oo, sympy.oo, 0.0 ], "Upper") + +## Free slip base (conditional) +Gamma = n_vect.sym # terrain_mesh.Gamma +bc_mask0 = sympy.Piecewise((1.0, z < -0.09), (0.0, True)) +bc_mask1 = sympy.Piecewise((1.0, -0.09 < z ), (0.0, True)) +bc_mask2 = sympy.Piecewise((1.0, z < 0.0 ), (0.0, True)) + +nbc = 10000 * sympy.simplify( + bc_mask0 * Gamma.dot(v.sym) * Gamma + + (bc_mask1 * bc_mask2) * v.sym + ) + +stokes.add_natural_bc(nbc, "Lower") + +## Buoyancy + +theta = 2 * sympy.pi / 180 +stokes.bodyforce = -sympy.Matrix([[sympy.sin(theta), 0.0, 0.0*sympy.cos(theta)]]) + +stokes.petsc_options.setValue("ksp_monitor", None) +stokes.petsc_options.setValue("snes_monitor", None) + +stokes.solve() +# - + + +terrain_mesh.write_timestep( + expt_name, + meshUpdates=True, + meshVars=[p, v], + outputPath=outputPath, + index=0, +) + + +# + +## Visualise the mesh + +# OR +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(terrain_mesh) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(v) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v.sym) + + clipped = pvmesh.clip(origin=(0.0, 0.0, -0.09), normal=(0.0, 0, 1), invert=True) + clipped.point_data["V"] = vis.vector_fn_to_pv_points(clipped, v.sym) + + clipped2 = pvmesh.clip(origin=(0.0, 0.0, -0.05), normal=(0.0, 0, 1), invert=True) + clipped2.point_data["V"] = vis.vector_fn_to_pv_points(clipped2, v.sym) + + clipped3 = pvmesh.clip(origin=(0.0, 0.0, 0.4), normal=(0.0, 0, 1), invert=False) + clipped3.point_data["V"] = vis.vector_fn_to_pv_points(clipped3, v.sym) + + + skip = 10 + points = np.zeros((terrain_mesh._centroids[::skip].shape[0], 3)) + points[:, 0] = terrain_mesh._centroids[::skip, 0] + points[:, 1] = terrain_mesh._centroids[::skip, 1] + points[:, 2] = terrain_mesh._centroids[::skip, 2] + + point_cloud = pv.PolyData(points[np.logical_and(points[:, 0] < 2.0, points[:, 0] > 0.0)] ) + + pvstream = pvmesh.streamlines_from_source( + point_cloud, vectors="V", + integration_direction="forward", + integrator_type=45, + surface_streamlines=False, + initial_step_length=0.1, + max_time=0.5, + max_steps=1000 + ) + + point_cloud2 = pv.PolyData(points[np.logical_and(points[:, 2] < 0.5, points[:, 2] > 0.45)] ) + + pvstream2 = pvmesh.streamlines_from_source( + point_cloud2, vectors="V", + integration_direction="forward", + integrator_type=45, + surface_streamlines=False, + initial_step_length=0.01, + max_time=0.5, + max_steps=1000 + ) + + pl = pv.Plotter(window_size=[1000, 1000]) + pl.add_axes() + + pl.add_mesh(pvmesh,'Grey', 'wireframe', opacity=0.1) + pl.add_mesh(clipped,'Blue', show_edges=False, opacity=0.25) + # pl.add_mesh(pvmesh, 'white', show_edges=True, opacity=0.5) + + #pl.add_mesh(pvstream) + pl.add_mesh(pvstream2) + + + arrows = pl.add_arrows(clipped2.points, clipped2.point_data["V"], + show_scalar_bar = False, opacity=1, + mag=100, ) + + # arrows = pl.add_arrows(clipped3.points, clipped3.point_data["V"], + # show_scalar_bar = False, opacity=1, + # mag=33, ) + + + # pl.screenshot(filename="sphere.png", window_size=(1000, 1000), return_img=False) + # OR + + pl.show(cpos="xy") + + + +# - + + + diff --git a/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Cartesian_SolC.py b/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Cartesian_SolC.py new file mode 100644 index 0000000..80159c4 --- /dev/null +++ b/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Cartesian_SolC.py @@ -0,0 +1,325 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Stokes Benchmark SolCx +# + + +# + +# %% +import petsc4py +from petsc4py import PETSc + +import nest_asyncio +nest_asyncio.apply() + +# options = PETSc.Options() +# options["help"] = None + +import os +os.environ["UW_TIMING_ENABLE"] = "1" + + +# + +import underworld3 as uw +from underworld3.systems import Stokes +from underworld3 import function +from underworld3 import timing + +import numpy as np +import sympy +from sympy import Piecewise + +# + +# %% +n_els = 4 +refinement = 2 + +mesh1 = uw.meshing.UnstructuredSimplexBox(regular=True, + minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0), cellSize=1 / n_els, + qdegree=3, refinement=refinement +) + +mesh2 = uw.meshing.StructuredQuadBox( + elementRes=(n_els, n_els), + minCoords=(0.0, 0.0), + maxCoords=(1.0, 1.0), + qdegree=3, + refinement=refinement +) + +mesh = mesh1 +x,y = mesh.X +# - + + +stokes = uw.systems.Stokes(mesh, verbose=False) + +# + +v = stokes.Unknowns.u +p = stokes.Unknowns.p + +stokes.constitutive_model=uw.constitutive_models.ViscousFlowModel(stokes.Unknowns) +stokes.constitutive_model.Parameters.shear_viscosity_0 = 1 +# %% +T = uw.discretisation.MeshVariable("T", mesh, 1, degree=3, continuous=True, varsymbol=r"{T}") +T2 = uw.discretisation.MeshVariable("T2", mesh, 1, degree=3, continuous=True, varsymbol=r"{T_2}") + +v0 = stokes.Unknowns.u.clone("V0", r"{v_0}") +v1 = v0.clone("V1", r"{v_1}") +# - + + +eta_0 = 1.0 +x_c = 0.5 +f_0 = 1.0 + + +stokes.penalty = 100.0 +stokes.bodyforce = sympy.Matrix( + [ + 0, + Piecewise( + (f_0, x > x_c), + (0.0, True), + ), + ] +) + +# + +# This is the other way to impose no vertical flow + +# stokes.add_natural_bc( [0.0,1e5*v.sym[1]], "Top") # Top "free slip / penalty" + + +# + +# free slip. + +stokes.add_dirichlet_bc((sympy.oo, 0.0), "Top") +stokes.add_dirichlet_bc((sympy.oo,0.0), "Bottom") +stokes.add_dirichlet_bc((0.0,sympy.oo), "Left") +stokes.add_dirichlet_bc((0.0,sympy.oo), "Right") +# - + + +# We may need to adjust the tolerance if $\Delta \eta$ is large + +stokes.tolerance = 1.0e-6 + +stokes.petsc_options["snes_monitor"]= None +stokes.petsc_options["ksp_monitor"] = None + + +# + +stokes.petsc_options["snes_type"] = "newtonls" +stokes.petsc_options["ksp_type"] = "fgmres" + +# stokes.petsc_options.setValue("fieldsplit_velocity_pc_type", "mg") +stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_type", "kaskade") +stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_cycle_type", "w") + +stokes.petsc_options["fieldsplit_velocity_mg_coarse_pc_type"] = "svd" +stokes.petsc_options[f"fieldsplit_velocity_ksp_type"] = "fcg" +stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_type"] = "chebyshev" +stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_max_it"] = 7 +stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_converged_maxits"] = None + +# gasm is super-fast ... but mg seems to be bulletproof +# gamg is toughest wrt viscosity + +stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "gamg") +stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "additive") +stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v") + +# # # mg, multiplicative - very robust ... similar to gamg, additive + +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "mg") +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "multiplicative") +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v") + + +# + +# stokes._setup_pointwise_functions(verbose=True) +# stokes._setup_discretisation(verbose=True) +# stokes.dm.ds.view() +# - + +# %% +# Solve time +stokes.solve() + +# ### Visualise it ! + +# + +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v.sym) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, stokes.bodyforce[1]) + pvmesh.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh, v.sym.dot(v.sym)) + + velocity_points = vis.meshVariable_to_pv_cloud(v) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v.sym) + + pl = pv.Plotter(window_size=(1000, 750)) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="Vmag", + use_transparency=False, + opacity=1.0, + ) + + arrows = pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=3.0, opacity=1, show_scalar_bar=False) + + pl.show(cpos="xy") + + +# - +# ## SolCx from the same setup + +# + +stokes.bodyforce = sympy.Matrix( + [0, -sympy.cos(sympy.pi * x) * sympy.sin(2 * sympy.pi * y)] +) + +viscosity_fn = sympy.Piecewise( + (1.0e6, x > x_c), + (1.0, True), +) + +stokes.constitutive_model.Parameters.shear_viscosity_0 = viscosity_fn +# - + + +stokes.constitutive_model.Parameters.shear_viscosity_0 + +stokes.saddle_preconditioner = sympy.simplify(1 / (stokes.constitutive_model.viscosity + stokes.penalty)) + +# + +timing.reset() +timing.start() +stokes.solve(zero_init_guess=True) +timing.print_table(display_fraction=0.999) + +# Save this solution + +with mesh.access(v0): + v0.data[...] = v.data[...] + +# + +# reset and re-do with natural bcs + +stokes._reset() +stokes.tolerance = 1.0e-6 +stokes.add_natural_bc([0.0,1e6*v.sym[1]], "Top") +stokes.add_dirichlet_bc((sympy.oo,0.0), "Bottom") +stokes.add_dirichlet_bc((0.0,sympy.oo), "Left") +stokes.add_dirichlet_bc((0.0,sympy.oo), "Right") +stokes.solve() + +with mesh.access(v1): + v1.data[...] = v.data[...] + +# + +# reset and re-do with natural bcs & petsc normals + +stokes._reset() +stokes.tolerance = 1.0e-6 + +Gamma = mesh.Gamma +stokes.add_natural_bc(1e6 * Gamma.dot(v.sym) * Gamma, "Top") +stokes.add_dirichlet_bc((sympy.oo,0.0), "Bottom") +stokes.add_dirichlet_bc((0.0,sympy.oo), "Left") +stokes.add_dirichlet_bc((0.0,sympy.oo), "Right") +stokes.solve() + + +# + +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh) + pvmesh.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh, v0.sym.dot(v0.sym)) + pvmesh.point_data["Visc"] = vis.scalar_fn_to_pv_points(pvmesh, stokes.constitutive_model.Parameters.shear_viscosity_0) + + pvmesh.point_data["V2"] = vis.vector_fn_to_pv_points(pvmesh, v.sym * stokes.constitutive_model.viscosity) + pvmesh.point_data["V0"] = vis.vector_fn_to_pv_points(pvmesh, v0.sym * stokes.constitutive_model.viscosity) + pvmesh.point_data["V1"] = vis.vector_fn_to_pv_points(pvmesh, v1.sym * stokes.constitutive_model.viscosity) + pvmesh.point_data["dV0"] = pvmesh.point_data["V1"] - pvmesh.point_data["V0"] + + velocity_points = vis.meshVariable_to_pv_cloud(v) + velocity_points.point_data["V2"] = vis.vector_fn_to_pv_points(velocity_points, v.sym) + velocity_points.point_data["V1"] = vis.vector_fn_to_pv_points(velocity_points, v1.sym) + velocity_points.point_data["V0"] = vis.vector_fn_to_pv_points(velocity_points, v0.sym) + velocity_points.point_data["dV1"] = velocity_points.point_data["V1"] - velocity_points.point_data["V0"] + velocity_points.point_data["dV2"] = velocity_points.point_data["V2"] - velocity_points.point_data["V0"] + + pl = pv.Plotter(window_size=(1000, 750)) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="Vmag", + use_transparency=False, + opacity=1.0, + ) + + arrows0 = pl.add_arrows(velocity_points.points, velocity_points.point_data["V2"], mag=100.0, opacity=1, show_scalar_bar=False) + arrows1 = pl.add_arrows(velocity_points.points, velocity_points.point_data["dV2"], mag=100000.0, opacity=1, show_scalar_bar=False) + + pl.show(jupyter_backend='client') + + + +# + +# %% +try: + import underworld as uw2 + + solC = uw2.function.analytic.SolC() + vel_soln_analytic = solC.fn_velocity.evaluate(mesh.data) + from mpi4py import MPI + + comm = MPI.COMM_WORLD + from numpy import linalg as LA + + with mesh.access(v): + num = function.evaluate(v.fn, mesh.data) # this appears busted + if comm.rank == 0: + print("Diff norm a. = {}".format(LA.norm(v.data - vel_soln_analytic))) + print("Diff norm b. = {}".format(LA.norm(num - vel_soln_analytic))) + # if not np.allclose(v.data, vel_soln_analytic, rtol=1): + # raise RuntimeError("Solve did not produce expected result.") + comm.barrier() +except ImportError: + import warnings + + warnings.warn("Unable to test SolC results as UW2 not available.") + +# %% diff --git a/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Cartesian_SolNL.py b/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Cartesian_SolNL.py new file mode 100644 index 0000000..a7e4b4d --- /dev/null +++ b/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Cartesian_SolNL.py @@ -0,0 +1,154 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Non-linear Stokes benchmark + +# %% +# To add a new cell, type '# %%' +# To add a new markdown cell, type '# %% [markdown]' +# %% +from petsc4py import PETSc +import underworld3 as uw +from underworld3.systems import Stokes +import numpy as np +import sympy +from mpi4py import MPI + +rank = MPI.COMM_WORLD.rank + +# + +# options = PETSc.Options() +# options["help"] = None + +# + +# options["ksp_rtol"] = 1.0e-8 +# options["snes_converged_reason"] = None +# options["snes_monitor"] = None +# # options["snes_monitor_short"] = None +# # options["snes_view"]=None +# options["snes_test_jacobian"] = None +# options["snes_rtol"] = 1.0e-7 +# # options["snes_max_it"] = 1 +# # options["snes_linesearch_monitor"] = None +# - + + +# %% +n_els = 32 +mesh = uw.meshing.UnstructuredSimplexBox( + minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0), cellSize=1/n_els +) + +# %% +# NL problem +# Create solution functions +from underworld3.function.analytic import ( + AnalyticSolNL_velocity, + AnalyticSolNL_bodyforce, + AnalyticSolNL_viscosity, +) + +x, y = mesh.X + +r = mesh.r +eta0 = 1.0 +n = 1 +r0 = 1.5 +params = (eta0, n, r0) +sol_bf_ijk = AnalyticSolNL_bodyforce(*params, *r) +sol_vel_ijk = AnalyticSolNL_velocity(*params, *r) + +sol_bf = mesh.vector.to_matrix(sol_bf_ijk) +sol_vel = mesh.vector.to_matrix(sol_vel_ijk) +sol_visc = AnalyticSolNL_viscosity(*params, *r) + +# debug - are problems just because there is no analytic solution module on mac +# The solNL case is a MMS force term (complicated) designed to produce a specific +# velocity field. This is a placeholder that just lets the non-linear problem run. + +# + +# sol_vel = sympy.Matrix([0, 0]) +# sol_bf = sympy.Matrix([0, sympy.cos(3 * sympy.pi * x) * sympy.cos(3 * sympy.pi * y)]) +# sol_visc = 1 +# - + +# %% +v = uw.discretisation.MeshVariable("U", mesh, mesh.dim, degree=1) +p = uw.discretisation.MeshVariable("P", mesh, 1, degree=0) + +stokes = uw.systems.Stokes(mesh, velocityField=v, pressureField=p) +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.shear_viscosity_0 = 1 + +# + +# BC + +stokes.add_dirichlet_bc(sol_vel, "Top", [0, 1]) +stokes.add_dirichlet_bc(sol_vel, "Bottom", [0, 1]) +stokes.add_dirichlet_bc(sol_vel, "Left", [0, 1]) +stokes.add_dirichlet_bc(sol_vel, "Right", [0, 1]) +# - + +stokes.petsc_options["snes_converged_reason"] = None +stokes.petsc_options["snes_monitor"] = None +stokes.petsc_options["ksp_monitor"] = None +stokes.petsc_options["snes_rtol"] = 1.0e-5 + + +stokes._setup_pointwise_functions(verbose=True) +stokes._setup_discretisation(verbose=True) +stokes.dm.ds.view() + +# + +# %% +# do linear first to get reasonable starting place +stokes.bodyforce = sol_bf +stokes.solve() +# %% +# get strainrate +sr = stokes.strainrate +# not sure if the following is needed as div_u should be zero +# sr -= (stokes.div_u / mesh.dim) * sympy.eye(mesh.dim) +# second invariant of strain rate + +inv2 = sr[0, 0] ** 2 + sr[0, 1] ** 2 + sr[1, 0] ** 2 + sr[1, 1] ** 2 +inv2 = 1 / 2 * inv2 +inv2 = sympy.sqrt(inv2) +alpha_by_two = 2 / r0 - 2 +# - +viscosity = 2 * eta0 * inv2**alpha_by_two + +stokes.constitutive_model.Parameters.shear_viscosity_0 = viscosity + +stokes.penalty = 1.0 +stokes.solve(zero_init_guess=False) + +sol_vel.T + +# %% +vdiff = stokes.u.sym - sol_vel.T +vdiff_dot_vdiff = uw.maths.Integral(mesh, vdiff.dot(vdiff)).evaluate() +v_dot_v = uw.maths.Integral(mesh, stokes.u.fn.dot(stokes.u.fn)).evaluate() + +import math + +rel_rms_diff = math.sqrt(vdiff_dot_vdiff / v_dot_v) +if rank == 0: + print(f"RMS diff = {rel_rms_diff}") + +if not np.allclose(rel_rms_diff, 0.00109, rtol=1.0e-2): + raise RuntimeError("Solve did not produce expected result.") + + + diff --git a/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Disk_CylCoords.py b/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Disk_CylCoords.py new file mode 100644 index 0000000..bf938d3 --- /dev/null +++ b/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Disk_CylCoords.py @@ -0,0 +1,333 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Cylindrical Stokes +# (In cylindrical coordinates) + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import petsc4py +from petsc4py import PETSc + +import underworld3 as uw +from underworld3 import timing +import numpy as np +import sympy + +from IPython.display import display +import os + +os.environ["UW_TIMING_ENABLE"] = "1" + +# + +# Define the problem size +# 1 - ultra low res for automatic checking +# 2 - low res problem to play with this notebook +# 3 - medium resolution (be prepared to wait) +# 4 - highest resolution (benchmark case from Spiegelman et al) + +problem_size = 2 + +# For testing and automatic generation of notebook output, +# over-ride the problem size if the UW_TESTING_LEVEL is set + +uw_testing_level = os.environ.get("UW_TESTING_LEVEL") +if uw_testing_level: + try: + problem_size = int(uw_testing_level) + except ValueError: + # Accept the default value + pass + +r_o = 1.0 +r_i = 0.5 +free_slip_upper = True + +if problem_size <= 1: + res = 0.1 +elif problem_size == 2: + res = 0.075 +elif problem_size == 3: + res = 0.05 +elif problem_size >= 4: + res = 0.01 + +# - + + +meshball_xyz_tmp = uw.meshing.Annulus( + radiusOuter=r_o, + radiusInner=r_i, + cellSize=res, + refinement=0, + filename="tmp_meshball.msh", +) + + +# + +## We don't have the native coordinates built in to this mesh + +xy_vec = meshball_xyz_tmp.dm.getCoordinates() +xy = xy_vec.array.reshape(-1, 2) + +dmplex = meshball_xyz_tmp.dm.clone() + +rtheta = np.empty_like(xy) +rtheta[:, 0] = np.sqrt(xy[:, 0] ** 2 + xy[:, 1] ** 2) +rtheta[:, 1] = np.arctan2(xy[:, 1] + 1.0e-16, xy[:, 0] + 1.0e-16) +rtheta_vec = xy_vec.copy() +rtheta_vec.array[...] = rtheta.reshape(-1)[...] +dmplex.setCoordinates(rtheta_vec) +# del meshball_xyz_tmp + +from enum import Enum +class boundaries(Enum): + Lower = 1 + Upper = 2 + Centre = 10 + +meshball = uw.meshing.Mesh( + dmplex, + boundaries = boundaries, + coordinate_system_type=uw.coordinates.CoordinateSystemType.CYLINDRICAL2D_NATIVE, + + qdegree=3, +) +uw.cython.petsc_discretisation.petsc_dm_set_periodicity( + meshball.dm, [0.0, 1.0], [0.0, 0.0], [0.0, 2 * np.pi] +) +meshball.dm.view() + +meshball_xyz = uw.meshing.Annulus( + radiusOuter=r_o, radiusInner=r_i, cellSize=res, qdegree=3 +) + +display(meshball_xyz.CoordinateSystem.type) +display(meshball_xyz.CoordinateSystem.N) +display(meshball_xyz.CoordinateSystem.R) +display(meshball_xyz.CoordinateSystem.r) +display(meshball_xyz.CoordinateSystem.X) +display(meshball_xyz.CoordinateSystem.x) + +display(meshball.CoordinateSystem.type) +display(meshball.CoordinateSystem.N) +display(meshball.CoordinateSystem.R) +display(meshball.CoordinateSystem.r) +display(meshball.CoordinateSystem.X) +display(meshball.CoordinateSystem.x) + +x, y = meshball.CoordinateSystem.X +r, t = meshball.CoordinateSystem.R + +v_soln = uw.discretisation.MeshVariable("U", meshball, 2, degree=2) +p_soln = uw.discretisation.MeshVariable("P", meshball, 1, degree=1, continuous=True) +p_cont = uw.discretisation.MeshVariable("Pc", meshball, 1, degree=2) + +v_soln_xy = uw.discretisation.MeshVariable("Uxy", meshball_xyz, 2, degree=2) +p_soln_xy = uw.discretisation.MeshVariable( + "Pxy", meshball_xyz, 1, degree=1, continuous=True +) +r_xy = uw.discretisation.MeshVariable("Rxy", meshball_xyz, 1, degree=1, continuous=True) + +# + +# Create a density structure / buoyancy force +# gravity will vary linearly from zero at the centre +# of the sphere to (say) 1 at the surface + +# Some useful coordinate stuff + +r, th = meshball.CoordinateSystem.R +x, y = meshball.CoordinateSystem.X + +unit_rvec = meshball.CoordinateSystem.unit_e_0 +gravity_fn = r / r_o + +# +Rayleigh = 1.0e5 + +# + +# Create Stokes object (r, theta) + +stokes = uw.systems.Stokes( + meshball, velocityField=v_soln, pressureField=p_soln, solver_name="stokes" +) + +stokes.tolerance = 1.0e-6 +stokes.petsc_options["snes_monitor"] = None + +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.shear_viscosity_0 = 1 + +# Velocity boundary conditions + +if not free_slip_upper: + stokes.add_dirichlet_bc(0.0, "Upper", 0) + stokes.add_dirichlet_bc(0.0, "Upper", 1) + +else: + stokes.add_dirichlet_bc(0.0, "Upper", 0) + +stokes.add_dirichlet_bc(0.0, "Lower", 0) +# stokes.add_dirichlet_bc(0.0, "Lower", 1) +# - + + +stokes.view() + +# #### Strain rate in Cylindrical (2D) geometry is this: +# +# $$ \dot\epsilon_{rr} = \frac{\partial u_r}{\partial r}$$ +# +# $$ \dot\epsilon_{\theta\theta} = \frac{1}{r} \frac{\partial u_\theta}{\partial \theta} + \frac{u_r}{r} $$ +# +# $$ 2\dot\epsilon_{r\theta} = \frac{1}{r} \frac{\partial u_r}{\partial \theta} + \frac{\partial u_\theta}{\partial r} - \frac{u_\theta}{r} $$ + +meshball.vector.strain_tensor(stokes.Unknowns.u.sym) + +# + +# Create Stokes object (x,y) + +radius_fn = meshball_xyz.CoordinateSystem.xR[0] +radius_fn = r_xy.sym[0] + +unit_rvec_xy = meshball_xyz.CoordinateSystem.unit_e_0 + +stokes_xy = uw.systems.Stokes( + meshball_xyz, + velocityField=v_soln_xy, + pressureField=p_soln_xy, + solver_name="stokes_xy", +) + +stokes_xy.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes_xy.constitutive_model.Parameters.shar_viscosity_0 = 1 +stokes_xy.petsc_options["snes_rtol"] = 1.0e-6 +stokes_xy.petsc_options["snes_monitor"] = None + +# Velocity boundary conditions + +if not free_slip_upper: + stokes_xy.add_dirichlet_bc(0.0, "Upper", 0) + stokes_xy.add_dirichlet_bc(0.0, "Upper", 1) +else: + print("Free slip !") + penalty = 100000 + stokes_xy.add_natural_bc( + penalty * unit_rvec_xy.dot(v_soln_xy.sym) * unit_rvec_xy, "Upper" + ) + stokes_xy.add_natural_bc( + penalty * unit_rvec_xy.dot(v_soln_xy.sym) * unit_rvec_xy, "Lower" + ) + +# stokes_xy.add_dirichlet_bc([0.0, 0.0], "Lower") +# - + +unit_rvec_xy + +penalty * unit_rvec_xy.dot(v_soln_xy.sym) * unit_rvec_xy + +# + +# t_init = 10.0 * sympy.exp(-5.0 * (x**2 + (y - 0.5) ** 2)) +t_init = sympy.cos(4 * th) +stokes.bodyforce = sympy.Matrix([Rayleigh * t_init, 0]) + +# ---- +t_init_xy = sympy.cos(4 * meshball_xyz.CoordinateSystem.xR[1]) +unit_rvec = meshball_xyz.CoordinateSystem.unit_e_0 +stokes_xy.bodyforce = Rayleigh * t_init_xy * unit_rvec + +# - + +stokes_xy.bodyforce + + + +with meshball_xyz.access(r_xy): + r_xy.data[:, 0] = uw.function.evaluate( + meshball_xyz.CoordinateSystem.xR[0], + coords=r_xy.coords, + coord_sys=meshball_xyz.N, + ) + +timing.start() +stokes.solve(zero_init_guess=True) +timing.print_table() + +timing.start() + +stokes_xy.solve(zero_init_guess=True) + +timing.print_table() + +U_xy = meshball.CoordinateSystem.xRotN * v_soln.sym.T + + +# + +# Visuals + +if uw.mpi.size == 1: + import underworld3.visualisation as vis # use this module for plotting + import pyvista as pv + import vtk + +pl = pv.Plotter(window_size=(1000, 1000)) + +pvmesh = uw.visualisation.mesh_to_pv_mesh(meshball_xyz) +pvmesh.point_data["T"] = uw.visualisation.scalar_fn_to_pv_points(pvmesh, t_init_xy) + +velocity_points = uw.visualisation.meshVariable_to_pv_cloud(v_soln_xy) +velocity_points_rt = uw.visualisation.meshVariable_to_pv_cloud(v_soln) + +velocity_points.point_data["Vxy"] = uw.visualisation.vector_fn_to_pv_points( + velocity_points, v_soln_xy.sym +) +velocity_points.point_data["Vrt"] = uw.visualisation.vector_fn_to_pv_points( + velocity_points_rt, U_xy.T +) + +pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="T", + use_transparency=False, + opacity=1.0, +) + +pl.add_arrows( + velocity_points.points, + velocity_points.point_data["Vxy"], + mag=1.0e-4, + opacity=0.75, + color="Black", +) +pl.add_arrows( + velocity_points.points, + velocity_points.point_data["Vrt"], + mag=1.0e-4, + opacity=0.75, + color="Green", +) + +pl.camera.SetPosition(0.75, 0.2, 1.5) +pl.camera.SetFocalPoint(0.75, 0.2, 0.0) +pl.camera.SetClippingRange(1.0, 8.0) + +pl.show() + + diff --git a/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Ellipse_Cartesian.py b/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Ellipse_Cartesian.py new file mode 100644 index 0000000..99ed6af --- /dev/null +++ b/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Ellipse_Cartesian.py @@ -0,0 +1,370 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Stokes (Cartesian formulation) in Elliptical Domain +# + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import petsc4py +from petsc4py import PETSc + +import underworld3 as uw +from underworld3.systems import Stokes +from underworld3 import function + +import numpy as np +import sympy + +import os +os.environ["UW_TIMING_ENABLE"] = "1" + +# + + +free_slip_upper = True +free_slip_lower = True + +# Define the problem size +# 1 - ultra low res for automatic checking +# 2 - low res problem to play with this notebook +# 3 - medium resolution (be prepared to wait) +# 4 - highest resolution (benchmark case from Spiegelman et al) + +problem_size = 3 + +# For testing and automatic generation of notebook output, +# over-ride the problem size if the UW_TESTING_LEVEL is set + +uw_testing_level = os.environ.get("UW_TESTING_LEVEL") +if uw_testing_level: + try: + problem_size = int(uw_testing_level) + except ValueError: + # Accept the default value + pass + +r_o = 1.0 +r_i = 0.5 + +if problem_size <= 1: + res = 0.5 +elif problem_size == 2: + res = 0.1 +elif problem_size == 3: + res = 0.05 +elif problem_size == 4: + res = 0.025 +elif problem_size == 5: + res = 0.01 +elif problem_size >= 6: + res = 0.005 + + +cellSizeOuter = res +cellSizeInner = res/2 +ellipticityOuter = 1.5 +ellipticityInner = 1.0 + +radiusOuter = r_o +radiusInner = r_i + + +# + +from enum import Enum + +class boundaries(Enum): + Inner = 1 + Outer = 2 + +if uw.mpi.rank == 0: + import gmsh + + gmsh.initialize() + gmsh.option.setNumber("General.Verbosity", 1) + gmsh.model.add("Annulus") + + p0 = gmsh.model.geo.add_point(0.00, 0.00, 0.00, meshSize=cellSizeInner) + + loops = [] + + p1 = gmsh.model.geo.add_point(radiusInner*ellipticityInner, 0.0, 0.0, meshSize=cellSizeInner) + p2 = gmsh.model.geo.add_point(0.0, radiusInner, 0.0, meshSize=cellSizeInner) + p3 = gmsh.model.geo.add_point(-radiusInner*ellipticityInner, 0.0, 0.0, meshSize=cellSizeInner) + p4 = gmsh.model.geo.add_point(0.0, -radiusInner, 0.0, meshSize=cellSizeInner) + + c1 = gmsh.model.geo.add_ellipse_arc(p1, p0, p1, p2) + c2 = gmsh.model.geo.add_ellipse_arc(p2, p0, p3, p3) + c3 = gmsh.model.geo.add_ellipse_arc(p3, p0, p3, p4) + c4 = gmsh.model.geo.add_ellipse_arc(p4, p0, p1, p1) + + cl1 = gmsh.model.geo.add_curve_loop([c1, c2, c3, c4], tag=boundaries.Inner.value) + + loops = [cl1] + loops + + p5 = gmsh.model.geo.add_point(radiusOuter*ellipticityOuter, 0.0, 0.0, meshSize=cellSizeOuter) + p6 = gmsh.model.geo.add_point(0.0, radiusOuter, 0.0, meshSize=cellSizeOuter) + p7 = gmsh.model.geo.add_point(-radiusOuter*ellipticityOuter, 0.0, 0.0, meshSize=cellSizeOuter) + p8 = gmsh.model.geo.add_point(0.0, -radiusOuter, 0.0, meshSize=cellSizeOuter) + + c5 = gmsh.model.geo.add_ellipse_arc(p5, p0, p5, p6) + c6 = gmsh.model.geo.add_ellipse_arc(p6, p0, p7, p7) + c7 = gmsh.model.geo.add_ellipse_arc(p7, p0, p7, p8) + c8 = gmsh.model.geo.add_ellipse_arc(p8, p0, p5, p5) + + # l1 = gmsh.model.geo.add_line(p5, p4) + + cl2 = gmsh.model.geo.add_curve_loop([c5, c6, c7, c8], tag=boundaries.Outer.value) + + loops = [cl2] + loops + + s = gmsh.model.geo.add_plane_surface(loops) + gmsh.model.geo.synchronize() + gmsh.model.mesh.embed(0, [p0], 2, s) + + gmsh.model.addPhysicalGroup( + 1, + [c1, c2, c3, c4], + boundaries.Inner.value, + name=boundaries.Inner.name, + ) + + gmsh.model.addPhysicalGroup( + 1, [c5, c6, c7, c8], + boundaries.Outer.value, + name=boundaries.Outer.name + ) + gmsh.model.addPhysicalGroup(2, [s], 666666, "Elements") + + gmsh.model.geo.synchronize() + + gmsh.model.mesh.generate(2) + gmsh.write("tmp_elliptical_mesh.msh") + gmsh.finalize() + +elliptical_mesh = uw.discretisation.Mesh( + "tmp_elliptical_mesh.msh", + degree=1, + qdegree=3, + useMultipleTags=True, + useRegions=True, + markVertices=True, + boundaries=boundaries, + coordinate_system_type=None, + refinement=0, + refinement_callback=None, + return_coords_to_bounds=None, + ) + +x,y = elliptical_mesh.X + +# - + + +elliptical_mesh.dm.view() + +# + +# Analytic expression for surface normals + +Gamma_N_Outer = sympy.Matrix([2 * x / ellipticityOuter**2, 2 * y ]).T +Gamma_N_Outer = Gamma_N_Outer / sympy.sqrt(Gamma_N_Outer.dot(Gamma_N_Outer)) +Gamma_N_Inner = sympy.Matrix([2 * x / ellipticityInner**2, 2 * y ]).T +Gamma_N_Inner = Gamma_N_Inner / sympy.sqrt(Gamma_N_Inner.dot(Gamma_N_Inner)) + + +# + +# Some geometry things + +x, y = elliptical_mesh.CoordinateSystem.X + +radius_fn = sympy.sqrt(x**2+y**2) +unit_rvec = elliptical_mesh.CoordinateSystem.X / radius_fn + +# Some useful coordinate stuff + + +# + +# Test that the second one is skipped + +v_soln = uw.discretisation.MeshVariable("U", elliptical_mesh, 2, degree=2, continuous=True, varsymbol=r"\mathbf{u}") +v_soln_1 = uw.discretisation.MeshVariable("U1", elliptical_mesh, 2, degree=2, continuous=True, varsymbol=r"{\mathbf{u}^[1]}") +v_soln_0 = uw.discretisation.MeshVariable("U0", elliptical_mesh, 2, degree=2, continuous=True, varsymbol=r"{\mathbf{u}^[0]}") +p_soln = uw.discretisation.MeshVariable("P", elliptical_mesh, 1, degree=1, continuous=True, varsymbol=r"\mathbf{p}") + + + +# + +n_vect = uw.discretisation.MeshVariable("Gamma", elliptical_mesh, 2, degree=2, varsymbol="{\Gamma_N}") + +projection = uw.systems.Vector_Projection(elliptical_mesh, n_vect) +projection.uw_function = sympy.Matrix([[0,0]]) + +# r.dot(Gamma) Ensure consistent orientation (not needed for mesh boundary surfaces) + +GammaNorm = unit_rvec.dot(elliptical_mesh.Gamma) / sympy.sqrt(elliptical_mesh.Gamma.dot(elliptical_mesh.Gamma)) + +projection.add_natural_bc(elliptical_mesh.Gamma * GammaNorm, "Outer") +projection.add_natural_bc(elliptical_mesh.Gamma * GammaNorm, "Inner") + +projection.solve() + +# Ensure n_vect are unit vectors +with elliptical_mesh.access(n_vect): + n_vect.data[:,:] /= np.sqrt(n_vect.data[:,0]**2 + n_vect.data[:,1]**2).reshape(-1,1) + + + +# + +# Create Stokes object + +stokes = Stokes(elliptical_mesh, velocityField=v_soln, pressureField=p_soln, solver_name="stokes") +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.shear_viscosity_0 = 1 +stokes.penalty = 1.0 +stokes.saddle_preconditioner = sympy.simplify(1 / (stokes.constitutive_model.viscosity + stokes.penalty)) + +stokes.petsc_options["fieldsplit_velocity_mg_coarse_pc_type"] = "svd" + +# Surface normals provided by DMPLEX + +Gamma = elliptical_mesh.Gamma +stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) * Gamma, "Outer") +stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) * Gamma, "Inner") + + +# + +t_init = sympy.cos(4 * sympy.atan2(y,x)) + +stokes.bodyforce = unit_rvec * t_init +stokes.petsc_options["ksp_monitor"] = None +stokes.petsc_options["snes_monitor"] = None +stokes.tolerance = 1.0e-6 + + +# + +from underworld3 import timing + +timing.reset() +timing.start() + +stokes.solve(zero_init_guess=True, debug=False) + +with elliptical_mesh.access(v_soln_1): + v_soln_1.data[...] = v_soln.data[...] + +timing.print_table() +# + +# Create Stokes object + +stokes._reset() + +# Surface normals (computed) + +stokes.add_natural_bc(10000 * n_vect.sym.dot(v_soln.sym) * n_vect.sym, "Outer") +stokes.add_natural_bc(10000 * n_vect.sym.dot(v_soln.sym) * n_vect.sym, "Inner") + + +stokes.solve(zero_init_guess=False) + +with elliptical_mesh.access(v_soln_0): + v_soln_0.data[...] = v_soln.data[...] + + +# + +# Create Stokes object + +stokes._reset() + +# Surface normals (computed analytically) +stokes.add_natural_bc(10000 * Gamma_N_Outer.dot(v_soln.sym) * Gamma_N_Outer, "Outer") +stokes.add_natural_bc(10000 * Gamma_N_Inner.dot(v_soln.sym) * Gamma_N_Inner, "Inner") + +stokes.solve(zero_init_guess=False) + +# + +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(elliptical_mesh) + velocity_points = vis.meshVariable_to_pv_cloud(v_soln) + velocity_points.point_data["Va"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym) + velocity_points.point_data["Vn"] = vis.vector_fn_to_pv_points(velocity_points, v_soln_1.sym) + velocity_points.point_data["Vp"] = vis.vector_fn_to_pv_points(velocity_points, v_soln_0.sym) + velocity_points.point_data["dVn"] = velocity_points.point_data["Vn"] - velocity_points.point_data["Va"] + velocity_points.point_data["dVp"] = velocity_points.point_data["Vp"] - velocity_points.point_data["Va"] + + + pvmesh.point_data["Va"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) + pvmesh.point_data["Vn"] = vis.vector_fn_to_pv_points(pvmesh, v_soln_0.sym) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_init) + + points = np.zeros((elliptical_mesh._centroids.shape[0], 3)) + points[:, 0] = elliptical_mesh._centroids[:, 0] + points[:, 1] = elliptical_mesh._centroids[:, 1] + point_cloud = pv.PolyData(points) + + pvstream = pvmesh.streamlines_from_source( + point_cloud, vectors="Va", + integration_direction="forward", + integrator_type=2, + surface_streamlines=True, + initial_step_length=0.01, + max_time=1.0, + max_steps=2000 + ) + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + scalars="T", + show_edges=True, + use_transparency=False, + opacity=0.75, + ) + pl.add_arrows(velocity_points.points, velocity_points.point_data["Va"], mag=3, color="Green") + pl.add_arrows(velocity_points.points, velocity_points.point_data["Vn"], mag=3, color="Black") + pl.add_arrows(velocity_points.points, velocity_points.point_data["Vp"], mag=3, color="Blue") + pl.add_arrows(velocity_points.points, velocity_points.point_data["dVp"], mag=1000, color="Yellow") + + pl.add_mesh(pvstream) + + pl.show(cpos="xy") +# - + +# From the `PETSc` docs, the form of the boundary integral (residual, jacobian, preconditioner) and the form of the interior integrals +# +# ## Neumann terms (boundary integrals) +# +# Boundary integral in mathematical form. +# +# $$\int_\Gamma \phi {\vec f}_0(u, u_t, \nabla u, x, t) \cdot \hat n + \nabla\phi \cdot {\overleftrightarrow f}_1(u, u_t, \nabla u, x, t) \cdot \hat n$$ +# +# +# ## Interior integrals +# +# $$\int_\Omega \phi f_0(u, u_t, \nabla u, x, t) + \nabla\phi \cdot {\vec f}_1(u, u_t, \nabla u, x, t)$$ +# +# +# + + diff --git a/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Sinker.py b/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Sinker.py new file mode 100644 index 0000000..dc3f42e --- /dev/null +++ b/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Sinker.py @@ -0,0 +1,420 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Multiple materials - Linear stokes sinker +# +# +# This is the notorious "Stokes sinker" problem in which we have a dense and "rigid" (highly viscous) blob sinking in a low-viscosity fluid. This combination of high velocity and low strain rate is challenging for iterative solvers and there is a limit to the viscosity jujmp that can be introduced before the solvers fail to converge. +# +# ![Sinker image with streamlines](images/SinkerSolution.png) +# +# We introduce the notion of an `IndexSwarmVariable` which automatically generates masks for a swarm +# variable that consists of discrete level values (integers). +# +# For a variable $M$, the mask variables are $\left\{ M^0, M^1 \ldots M^{N-1} \right\}$ where $N$ is the number of indices (e.g. material types) on the variable. This value *must be defined in advance*. +# +# The masks are orthogonal in the sense that $M^i * M^j = 0$ if $i \ne j$, and they are complete in the sense that $\sum_i M^i = 1$ at all points. +# +# The masks are implemented as continuous mesh variables (the user can specify the interpolation order) and so they are also differentiable (once). + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +# %% +from petsc4py import PETSc +import underworld3 as uw +from underworld3.systems import Stokes +import numpy as np +import sympy +from mpi4py import MPI + +import os + +os.environ["UW_TIMING_ENABLE"] = "1" + +if uw.mpi.size == 1: + os.makedirs("output", exist_ok=True) +else: + os.makedirs(f"output_np{uw.mpi.size}", exist_ok=True) +# - + + +uw + +# + +# Define the problem size +# 1 - ultra low res for automatic checking +# 2 - low res problem to play with this notebook +# 3 - medium resolution (be prepared to wait) +# 4 - highest resolution (benchmark case from Spiegelman et al) + +problem_size = 2 + +# For testing and automatic generation of notebook output, +# over-ride the problem size if the UW_TESTING_LEVEL is set + +uw_testing_level = os.environ.get("UW_TESTING_LEVEL") +if uw_testing_level: + try: + problem_size = int(uw_testing_level) + except ValueError: + # Accept the default value + pass +# - + +sys = PETSc.Sys() +sys.pushErrorHandler("traceback") + + +if problem_size <= 1: + res = 8 +elif problem_size == 2: + res = 16 +elif problem_size == 3: + res = 32 +elif problem_size == 4: + res = 48 +elif problem_size == 5: + res = 64 +elif problem_size >= 6: + res = 128 + + +# Set size and position of dense sphere. +sphereRadius = 0.1 +sphereCentre = (0.0, 0.7) + +# define some names for our index +materialLightIndex = 0 +materialHeavyIndex = 1 + +# Set constants for the viscosity and density of the sinker. +viscBG = 1.0 +viscSphere = 1.0e6 + +expt_name = f"output/stinker_eta{viscSphere}_rho10_res{res}" + +densityBG = 1.0 +densitySphere = 10.0 + +# location of tracer at bottom of sinker +x_pos = sphereCentre[0] +y_pos = sphereCentre[1] - sphereRadius + +nsteps = 0 + +swarmGPC = 2 + +mesh = uw.meshing.UnstructuredSimplexBox( + minCoords=(-1.0, 0.0), + maxCoords=(1.0, 1.0), + cellSize=1.0 / res, + regular=False, + qdegree=3, +) + +# ## Create Stokes object + +# + +stokes = uw.systems.Stokes(mesh) + +v = stokes.Unknowns.u +p = stokes.Unknowns.p + +# Set some options +stokes.penalty = 1.0 + +# Set some bcs +stokes.add_dirichlet_bc((sympy.oo, 0.0), "Top") +stokes.add_dirichlet_bc((sympy.oo, 0.0), "Bottom") +stokes.add_dirichlet_bc((0.0,sympy.oo), "Left") +stokes.add_dirichlet_bc((0.0,sympy.oo), "Right") +# - + + +swarm = uw.swarm.Swarm(mesh=mesh) +material = uw.swarm.IndexSwarmVariable( + "M", swarm, indices=2, proxy_continuous=False, proxy_degree=1 +) +swarm.populate(fill_param=4) + +blob = np.array([[sphereCentre[0], sphereCentre[1], sphereRadius, 1]]) + + +with swarm.access(material): + material.data[...] = materialLightIndex + + for i in range(blob.shape[0]): + cx, cy, r, m = blob[i, :] + inside = (swarm.data[:, 0] - cx) ** 2 + (swarm.data[:, 1] - cy) ** 2 < r**2 + material.data[inside] = m + +# %% +tracer = np.zeros(shape=(1, 2)) +tracer[:, 0], tracer[:, 1] = x_pos, y_pos + +density = densityBG * material.sym[0] + densitySphere * material.sym[1] +viscosity = viscBG * material.sym[0] + viscSphere * material.sym[1] + + +# + +# viscosity = sympy.Max( sympy.Min(viscosityMat, eta_max), eta_min) + +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.shear_viscosity_0 = viscosity +stokes.bodyforce = sympy.Matrix([0, -1 * density]) + +# - + +render = True + + + + +# + +import pyvista as pv +pl = pv.Plotter(notebook=True) +pl.camera.position = (1.1, 1.5, 0.0) +pl.camera.focal_point = (0.2, 0.3, 0.3) +pl.camera.up = (0.0, 1.0, 0.0) +pl.camera.zoom(1.4) + +def plot_T_mesh(filename): + if not render: + return + + import numpy as np + import pyvista as pv + import underworld3.visualisation + + pvmesh = uw.visualisation.mesh_to_pv_mesh(mesh) + point_cloud = underworld3.visualisation.swarm_to_pv_cloud(swarm) + + with swarm.access(): + point_cloud.point_data["M"] = material.data.copy() + + ## Plotting into existing pl (memory leak in panel code) + pl.clear() + + pl.add_mesh(pvmesh, "Black", "wireframe") + + pl.add_points( + point_cloud, + cmap="coolwarm", + render_points_as_spheres=False, + point_size=10, + opacity=0.5, + ) + + pl.screenshot( + filename="{}.png".format(filename), window_size=(1280, 1280), return_img=False + ) + + +# + +# stokes.petsc_options.view() + +snes_rtol = 1.0e-6 +stokes.tolerance = snes_rtol + +# stokes.petsc_options["snes_converged_reason"] = None +# stokes.petsc_options["ksp_type"] = "gmres" +# stokes.petsc_options["ksp_rtol"] = 1.0e-9 +# stokes.petsc_options["ksp_atol"] = 1.0e-12 +# stokes.petsc_options["fieldsplit_pressure_ksp_rtol"] = 1.0e-8 +# stokes.petsc_options["fieldsplit_velocity_ksp_rtol"] = 1.0e-8 +# stokes.petsc_options["snes_atol"] = 0.1 * snes_rtol # by inspection +stokes.petsc_options["ksp_monitor"] = None + + +# - + + +nstep = 15 + +step = 0 +time = 0.0 +nprint = 0.0 + +# %% +tSinker = np.zeros(nstep) +ySinker = np.zeros(nstep) + + + +# + +from underworld3 import timing + +timing.reset() +timing.start() +stokes.solve(zero_init_guess=True) +timing.print_table() +# - + +while step < nstep: + ### Get the position of the sinking ball + ymin = tracer[:, 1].min() + ySinker[step] = ymin + tSinker[step] = time + + ### estimate dt + dt = stokes.estimate_dt() + if uw.mpi.rank == 0: + print(f"dt = {dt}", flush=True) + + ## This way should be a bit safer in parallel where particles can move + ## processors in the middle of the calculation if you are not careful + ## PS - the function.evaluate needs fixing to take sympy.Matrix functions + + swarm.advection(stokes.u.sym, dt, corrector=True) + + ### solve stokes + stokes.solve(zero_init_guess=False) + + ### print some stuff + if uw.mpi.size == 1: + print(f"Step: {str(step).rjust(3)}, time: {time:6.2f}, tracer: {ymin:6.2f}") + plot_T_mesh(filename="{}_step_{}".format(expt_name, step)) + + mesh.write_timestep("stokesSinker", meshUpdates=False, meshVars=[p, v], index=step) + + step += 1 + time += dt + + +# %% +if uw.mpi.rank == 0: + print("Initial position: t = {0:.3f}, y = {1:.3f}".format(tSinker[0], ySinker[0])) + print( + "Final position: t = {0:.3f}, y = {1:.3f}".format( + tSinker[nsteps - 1], ySinker[nsteps - 1] + ) + ) + + import matplotlib.pyplot as pyplot + + fig = pyplot.figure() + fig.set_size_inches(12, 6) + ax = fig.add_subplot(1, 1, 1) + ax.plot(tSinker, ySinker) + ax.set_xlabel("Time") + ax.set_ylabel("Sinker position") + +# + +import numpy as np +import pyvista as pv +import underworld3 as uw +import underworld3.visualisation + +pvmesh = uw.visualisation.mesh_to_pv_mesh(mesh) +pvmesh.point_data["V"] = uw.visualisation.vector_fn_to_pv_points(pvmesh, v.sym) +pvmesh.point_data["rho"] = uw.function.evaluate(density, mesh.data) + +swarm_points = underworld3.visualisation.swarm_to_pv_cloud(swarm) +swarm_points.point_data["M"] = uw.visualisation.scalar_fn_to_pv_points(swarm_points, material.visMask()) + +velocity_points = underworld3.visualisation.meshVariable_to_pv_cloud(v) +velocity_points.point_data["X"] = uw.visualisation.coords_to_pv_coords(v.coords) +velocity_points.point_data["V"] = uw.visualisation.vector_fn_to_pv_points(velocity_points, v.sym) +# - +# ## check if that worked + +if uw.mpi.size == 1: + + import numpy as np + import pyvista as pv + import underworld3 as uw + import underworld3.visualisation + + pvmesh = uw.visualisation.mesh_to_pv_mesh(mesh) + pvmesh.point_data["V"] = uw.visualisation.vector_fn_to_pv_points(pvmesh, v.sym) + pvmesh.point_data["rho"] = uw.function.evaluate(density, mesh.data) + + swarm_points = underworld3.visualisation.swarm_to_pv_cloud(swarm) + swarm_points.point_data["M"] = uw.visualisation.scalar_fn_to_pv_points(swarm_points, material.visMask()) + + velocity_points = underworld3.visualisation.meshVariable_to_pv_cloud(v) + velocity_points.point_data["V"] = uw.visualisation.vector_fn_to_pv_points(velocity_points, v.sym) + + pvstream = pvmesh.streamlines_from_source( + swarm_points, + vectors="V", + integration_direction="both", + max_steps=10, + surface_streamlines=True, + max_step_length=0.05, + ) + + pl = pv.Plotter(window_size=(1000, 750)) + + pl.add_mesh(pvmesh, "Black", "wireframe") + + streamlines = pl.add_mesh(pvstream, opacity=0.25) + streamlines.SetVisibility(False) + + + pl.add_mesh( + swarm_points, + cmap="coolwarm", + edge_color="Black", + show_edges=False, + scalars="M", + use_transparency=False, + point_size=2.0, + opacity=0.5, + show_scalar_bar=False, + ) + + pl.add_mesh( + velocity_points, + + ) + + arrows = pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=3.0, opacity=0.33, show_scalar_bar=False) + + + ## Widgets + + def toggle_streamlines(flag): + streamlines.SetVisibility(flag) + + def toggle_arrows(flag): + arrows.SetVisibility(flag) + + pl.add_checkbox_button_widget(toggle_streamlines, value=False, size = 10, position = (10, 20)) + pl.add_checkbox_button_widget(toggle_arrows, value=False, size = 10, position = (30, 20)) + + + + # pl.screenshot(filename="SinkerSolution_hr.png", window_size=(4000, 2000)) + + + + + + pl.show(cpos="xy") + +velocity_points.point_data["V"] + +uw.function.evalf(v.sym[0], velocity_points.points[:,0:2]) + +velocity_points.point_data["V"] + +# + + +velocity_points.points.min() +# - + diff --git a/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Spherical_Cap.py b/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Spherical_Cap.py new file mode 100644 index 0000000..5516750 --- /dev/null +++ b/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Spherical_Cap.py @@ -0,0 +1,325 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Stokes flow in a Spherical Domain +# +# +# ## Mathematical formulation +# +# The Navier-Stokes equation describes the time-dependent flow of a viscous fluid in response to buoyancy forces and pressure gradients: +# +# $$ +# \rho \frac{\partial \mathbf{u}}{\partial t} + \eta\nabla^2 \mathbf{u} -\nabla p = \rho \mathbf{g} +# $$ +# +# Where $\rho$ is the density, $\eta$ is dynamic viscosity and $\mathbf{g}$ is the gravitational acceleration vector. We here assume that density changes are due to temperature and are small enough to be consistent with an assumption of incompressibility (the Boussinesq approximation). We can rescale this equation of motion using units for length, time, temperature and mass that are specific to the problem and, in this way, obtain a scale-independent form: +# +# $$ +# \frac{1}{\mathrm{Pr}} \frac{\partial \mathbf{u}}{\partial t} + \nabla^2 \mathbf{u} -\nabla p = \mathrm{Ra} T' \hat{\mathbf{g}} +# $$ +# + +# where we have assumed that buoyancy forces on the right hand side are due to temperature variations, and the two dimensionless numbers, $\mathrm{Ra}$ and $\mathrm{Pr}$ are measures of the importance of buoyancy forcing and intertial terms, respectively. +# +# $$ +# \mathrm{Ra} = \frac{g\rho_0 \alpha \Delta T d^3}{\kappa \eta} +# \quad \textrm{and} \quad +# \mathrm{Pr} = \frac{\eta}{\rho \kappa} +# $$ +# +# Here $\alpha$ is the thermal expansivity, $\Delta T$ is the range of the temperature variation, $d$ is the typical length scale over which temperature varies, $\kappa$ is the thermal diffusivity ( $\kappa = k / \rho_0 C_p$; $k$ is thermal conductivity, and $C_p$ is heat capacity). + +# If we assume that the Prandtl number is large, then the inertial terms will not contribute significantly to the balance of forces in the equation of motion because we have rescaled the equations so that the velocity and pressure gradient terms are of order 1. This assumption eliminates the time dependent terms in the equations and tells us that the flow velocity and pressure field are always in equilibrium with the pattern of density variations and this also tells us that we can evaluate the flow without needing to know the history or origin of the buoyancy forces. When the viscosity is independent of velocity and dynamic pressure, the velocity and pressure scale proportionally with $\mathrm{Ra}$ but the flow pattern itself is unchanged. +# +# The scaling that we use for the non-dimensionalisation is as follows: +# +# $$ +# x = d x', \quad t = \frac{d^2}{\kappa} t', \quad T=\Delta T T', \quad +# p = p_0 + \frac{\eta \kappa}{d^2} p' +# $$ +# +# where the stress (pressure) scaling using viscosity ($\eta$) determines how the mass scales. In the above, $d$ is the radius of the inner core, a typical length scale for the problem, $\Delta T$ is the order-of-magnitude range of the temperature variation from our observations, and $\kappa$ is thermal diffusivity. The scaled velocity is obtained as $v = \kappa / d v'$. + +# ## Formulation & model +# +# +# The model consists of a spherical ball divided into an unstructured tetrahedral mesh of quadratic velocity, linear pressure elements with a free slip upper boundary and with a buoyancy force pre-defined : +# +# $$ +# T(r,\theta,\phi) = T_\textrm{TM}(\theta, \phi) \cdot r \sin(\pi r) +# $$ + +# ## Computational script in python + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import petsc4py +from petsc4py import PETSc +import mpi4py +import os + +os.environ["UW_TIMING_ENABLE"] = "1" + +import underworld3 as uw +import numpy as np +import sympy + +if uw.mpi.size == 1: + os.makedirs("output", exist_ok=True) +else: + os.makedirs(f"output_np{uw.mpi.size}", exist_ok=True) + + +# + +# Define the problem size +# 1 - ultra low res for automatic checking +# 2 - low res problem to play with this notebook +# 3 - medium resolution (be prepared to wait) +# 4 - highest resolution (benchmark case from Spiegelman et al) + +problem_size = uw.options.getInt("problem_size", default=4) +grid_refinement = uw.options.getInt("grid_refinement", default=0) +grid_simplex = uw.options.getBool("simplex", default=True) + + +# + +visuals = 1 +output_dir = "output" + +# Some gmsh issues, so we'll use a pre-built one +r_o = 1.0 +r_i = 0.547 + +Rayleigh = 1.0e6 # Doesn't actually matter to the solution pattern, + +# + +if problem_size <= 1: + els = 3 +elif problem_size == 2: + els = 6 +elif problem_size == 3: + els = 12 +elif problem_size == 4: + els = 24 +elif problem_size == 5: # Pretty extreme to mesh this on proc0 + els = 48 +elif problem_size >= 6: # should consider refinement (or prebuild) + els = 96 + +cell_size = 1/els +res = cell_size + +expt_name = f"Stokes_Spherical_Cap_free_slip_{els}" + +from underworld3 import timing + +timing.reset() +timing.start() + +# + + +meshball = uw.meshing.RegionalSphericalBox( + radiusInner=r_i, + radiusOuter=r_o, + numElements=els, + refinement=grid_refinement, + qdegree=2, + simplex=grid_simplex, + ) + +meshball.dm.view() +# - +stokes = uw.systems.Stokes( + meshball, + verbose=False, + solver_name="stokes", +) + + +v_soln = stokes.Unknowns.u +p_soln = stokes.Unknowns.p + +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.viscosity = 1 +stokes.penalty = 1.0 + +# + +# Create a density structure / buoyancy force +# gravity will vary linearly from zero at the centre +# of the sphere to (say) 1 at the surface + +# Some useful coordinate stuff + +x, y, z = meshball.CoordinateSystem.N +ra, l1, l2 = meshball.CoordinateSystem.R + +## Mesh Variables for T ## + +radius_fn = sympy.sqrt( + meshball.rvec.dot(meshball.rvec) +) # normalise by outer radius if not 1.0 + +unit_rvec = meshball.X / (radius_fn) +gravity_fn = radius_fn + +## Buoyancy (T) field + +t_soln = uw.discretisation.MeshVariable(r"\Delta T", meshball, 1, degree=2) + +t_forcing_fn = 1.0 * ( + sympy.exp(-10.0 * (x**2 + (y - 0.8) ** 2 + z**2)) + + sympy.exp(-10.0 * ((x - 0.8) ** 2 + y**2 + z**2)) + + sympy.exp(-10.0 * (x**2 + y**2 + (z + 0.8) ** 2)) +) + +with meshball.access(t_soln): + t_soln.data[...] = uw.function.evaluate( + t_forcing_fn, t_soln.coords, meshball.N + ).reshape(-1, 1) + + +# + +# Stokes settings + +stokes.tolerance = 1.0e-3 +stokes.petsc_options["ksp_monitor"] = None + +# stokes.petsc_options["ksp_type"] = "fgmres" + +# # stokes.petsc_options.setValue("fieldsplit_velocity_pc_type", "mg") +# stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_type", "kaskade") +# stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_cycle_type", "w") + +# stokes.petsc_options["fieldsplit_velocity_mg_coarse_pc_type"] = "svd" + +# stokes.petsc_options[f"fieldsplit_velocity_ksp_type"] = "fcg" +# stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_type"] = "chebyshev" +# stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_max_it"] = 5 +# stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_converged_maxits"] = None + +# # gasm is super-fast ... but mg seems to be bulletproof +# # gamg is toughest wrt viscosity + +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "gamg") +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "additive") +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v") + +# # # mg, multiplicative - very robust ... similar to gamg, additive + +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "mg") +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "multiplicative") +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v") +# thermal buoyancy force + +Gamma = meshball.Gamma +stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) * Gamma, "Upper") +stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) * Gamma, "Lower") +stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) * Gamma, "North") +stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) * Gamma, "East") +stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) * Gamma, "South") +stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) * Gamma, "West") + +stokes.bodyforce = unit_rvec * Rayleigh * gravity_fn * t_forcing_fn + +# + +timing.reset() +timing.start() + +stokes.solve(zero_init_guess=True) +# - +timing.print_table() + +# + +outdir="output" + +meshball.write_timestep( + expt_name, + meshUpdates=True, + meshVars=[p_soln, v_soln], + outputPath=outdir, + index=0, +) + + + +# + +# OR +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym) + + clipped = pvmesh.clip(origin=(0.0, 0.0, 0.0), normal=(0.0, 1, 0), invert=True) + + points = np.zeros((meshball._centroids.shape[0], 3)) + points[:, 0] = meshball._centroids[:, 0] + points[:, 1] = meshball._centroids[:, 1] + points[:, 2] = meshball._centroids[:, 2] + + point_cloud = pv.PolyData(points) + + pvstream = pvmesh.streamlines_from_source( + point_cloud, vectors="V", + integration_direction="forward", + integrator_type=45, + surface_streamlines=False, + initial_step_length=0.01, + max_time=0.25, + max_steps=1000 + ) + + pl = pv.Plotter(window_size=[1000, 750]) + pl.add_axes() + + pl.add_mesh( + clipped, + # cmap="RdGy_r", + cmap="coolwarm", + edge_color="Black", + show_edges=False, + scalars="T", + use_transparency=False, + show_scalar_bar = False, + opacity=1, + ) + + # pl.add_mesh(pvmesh, cmap="coolwarm", edge_color="Black", show_edges=True, scalars="T", + # use_transparency=False, opacity=1.0) + + + pl.add_mesh(pvstream) + + arrows = pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], + show_scalar_bar = False, + mag=10/Rayleigh, ) + + # pl.screenshot(filename="sphere.png", window_size=(1000, 1000), return_img=False) + # OR + pl.show(cpos="xy") + +# - + diff --git a/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Spherical_FreeSlipBCs.py b/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Spherical_FreeSlipBCs.py new file mode 100644 index 0000000..7703090 --- /dev/null +++ b/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Spherical_FreeSlipBCs.py @@ -0,0 +1,432 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Stokes flow in a Spherical Domain +# +# +# ## Mathematical formulation +# +# The Navier-Stokes equation describes the time-dependent flow of a viscous fluid in response to buoyancy forces and pressure gradients: +# +# $$ +# \rho \frac{\partial \mathbf{u}}{\partial t} + \eta\nabla^2 \mathbf{u} -\nabla p = \rho \mathbf{g} +# $$ +# +# Where $\rho$ is the density, $\eta$ is dynamic viscosity and $\mathbf{g}$ is the gravitational acceleration vector. We here assume that density changes are due to temperature and are small enough to be consistent with an assumption of incompressibility (the Boussinesq approximation). We can rescale this equation of motion using units for length, time, temperature and mass that are specific to the problem and, in this way, obtain a scale-independent form: +# +# $$ +# \frac{1}{\mathrm{Pr}} \frac{\partial \mathbf{u}}{\partial t} + \nabla^2 \mathbf{u} -\nabla p = \mathrm{Ra} T' \hat{\mathbf{g}} +# $$ +# + +# where we have assumed that buoyancy forces on the right hand side are due to temperature variations, and the two dimensionless numbers, $\mathrm{Ra}$ and $\mathrm{Pr}$ are measures of the importance of buoyancy forcing and intertial terms, respectively. +# +# $$ +# \mathrm{Ra} = \frac{g\rho_0 \alpha \Delta T d^3}{\kappa \eta} +# \quad \textrm{and} \quad +# \mathrm{Pr} = \frac{\eta}{\rho \kappa} +# $$ +# +# Here $\alpha$ is the thermal expansivity, $\Delta T$ is the range of the temperature variation, $d$ is the typical length scale over which temperature varies, $\kappa$ is the thermal diffusivity ( $\kappa = k / \rho_0 C_p$; $k$ is thermal conductivity, and $C_p$ is heat capacity). + +# If we assume that the Prandtl number is large, then the inertial terms will not contribute significantly to the balance of forces in the equation of motion because we have rescaled the equations so that the velocity and pressure gradient terms are of order 1. This assumption eliminates the time dependent terms in the equations and tells us that the flow velocity and pressure field are always in equilibrium with the pattern of density variations and this also tells us that we can evaluate the flow without needing to know the history or origin of the buoyancy forces. When the viscosity is independent of velocity and dynamic pressure, the velocity and pressure scale proportionally with $\mathrm{Ra}$ but the flow pattern itself is unchanged. +# +# The scaling that we use for the non-dimensionalisation is as follows: +# +# $$ +# x = d x', \quad t = \frac{d^2}{\kappa} t', \quad T=\Delta T T', \quad +# p = p_0 + \frac{\eta \kappa}{d^2} p' +# $$ +# +# where the stress (pressure) scaling using viscosity ($\eta$) determines how the mass scales. In the above, $d$ is the radius of the inner core, a typical length scale for the problem, $\Delta T$ is the order-of-magnitude range of the temperature variation from our observations, and $\kappa$ is thermal diffusivity. The scaled velocity is obtained as $v = \kappa / d v'$. + +# ## Formulation & model +# +# +# The model consists of a spherical ball divided into an unstructured tetrahedral mesh of quadratic velocity, linear pressure elements with a free slip upper boundary and with a buoyancy force pre-defined : +# +# $$ +# T(r,\theta,\phi) = T_\textrm{TM}(\theta, \phi) \cdot r \sin(\pi r) +# $$ + +# ## Computational script in python + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import petsc4py +from petsc4py import PETSc +import mpi4py +import os + +os.environ["UW_TIMING_ENABLE"] = "1" + +import underworld3 as uw +import numpy as np +import sympy + +if uw.mpi.size == 1: + os.makedirs("output", exist_ok=True) +else: + os.makedirs(f"output_np{uw.mpi.size}", exist_ok=True) + + +# + +# Define the problem size +# 1 - ultra low res for automatic checking +# 2 - low res problem to play with this notebook +# 3 - medium resolution (be prepared to wait) +# 4 - highest resolution (benchmark case from Spiegelman et al) + +problem_size = uw.options.getInt("problem_size", default=3) +grid_refinement = uw.options.getInt("grid_refinement", default=0) +grid_type = uw.options.getString("grid_type", default="simplex") + + +# + +visuals = 1 +output_dir = "output" +grid_type = "simplex_sphere" + +# Some gmsh issues, so we'll use a pre-built one +r_o = 1.0 +r_i = 0.547 + +Rayleigh = 1.0e6 # Doesn't actually matter to the solution pattern, + +# + +if problem_size <= 1: + els = 3 +elif problem_size == 2: + els = 6 +elif problem_size == 3: + els = 12 +elif problem_size == 4: + els = 50 + cell_size = 0.02 +elif problem_size == 5: # Pretty extreme to mesh this on proc0 + els = 66 +elif problem_size >= 6: # should consider refinement (or prebuild) + els = 100 + +cell_size = 1/els +res = cell_size + +expt_name = f"Stokes_Sphere_free_slip_{els}" + +from underworld3 import timing + +timing.reset() +timing.start() + +# + +if "ball" in grid_type: + meshball = uw.meshing.SegmentedSphericalBall( + radius=r_o, + cellSize=cell_size, + numSegments=5, + qdegree=2, + refinement=grid_refinement, + ) +elif "cubed" in grid_type: + meshball = uw.meshing.CubedSphere( + radiusInner=r_i, + radiusOuter=r_o, + numElements=els, + refinement=grid_refinement, + qdegree=2, + ) +else: + meshball = uw.meshing.SegmentedSphericalShell( + radiusInner=r_i, + radiusOuter=r_o, + cellSize=cell_size, + numSegments=5, + qdegree=2, + refinement=grid_refinement, + ) + +meshball.dm.view() +# - +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + + clipped = pvmesh.clip(origin=(0.0, 0.0, 0.0), normal=(0.0, 0.0, 1.0), invert=True, crinkle=True) + + pl = pv.Plotter(window_size=[1000, 1000]) + pl.add_axes() + + pl.add_mesh( + clipped, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + use_transparency=False, + show_scalar_bar = False, + opacity=1.0, + ) + + pl.show(cpos="xy") + + + + +stokes = uw.systems.Stokes( + meshball, + verbose=False, + solver_name="stokes", +) + + +v_soln = stokes.Unknowns.u +p_soln = stokes.Unknowns.p + +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.viscosity = 1 +stokes.penalty = 0.0 + +# + +# Create a density structure / buoyancy force +# gravity will vary linearly from zero at the centre +# of the sphere to (say) 1 at the surface + +# Some useful coordinate stuff + +x, y, z = meshball.CoordinateSystem.N +ra, l1, l2 = meshball.CoordinateSystem.R + +## Mesh Variables for T ## + +radius_fn = sympy.sqrt( + meshball.rvec.dot(meshball.rvec) +) # normalise by outer radius if not 1.0 + +unit_rvec = meshball.X / (radius_fn) +gravity_fn = radius_fn + +## Buoyancy (T) field + +t_soln = uw.discretisation.MeshVariable(r"\Delta T", meshball, 1, degree=2) + +t_forcing_fn = 1.0 * ( + sympy.exp(-10.0 * (x**2 + (y - 0.8) ** 2 + z**2)) + + sympy.exp(-10.0 * ((x - 0.8) ** 2 + y**2 + z**2)) + + sympy.exp(-10.0 * (x**2 + y**2 + (z - 0.8) ** 2)) +) + +with meshball.access(t_soln): + t_soln.data[...] = uw.function.evaluate( + t_forcing_fn, t_soln.coords, meshball.N + ).reshape(-1, 1) + + +# + +# Rigid body rotations that are null-spaces for this set of bc's + +# We can remove these after the fact, but also useful to double check +# that we are not adding anything to excite these modes in the forcing terms. + +orientation_wrt_z = sympy.atan2(y + 1.0e-10, x + 1.0e-10) +v_rbm_z_x = -ra * sympy.sin(orientation_wrt_z) +v_rbm_z_y = ra * sympy.cos(orientation_wrt_z) +v_rbm_z = sympy.Matrix([v_rbm_z_x, v_rbm_z_y, 0]).T + +orientation_wrt_x = sympy.atan2(z + 1.0e-10, y + 1.0e-10) +v_rbm_x_y = -ra * sympy.sin(orientation_wrt_x) +v_rbm_x_z = ra * sympy.cos(orientation_wrt_x) +v_rbm_x = sympy.Matrix([0, v_rbm_x_y, v_rbm_x_z]).T + +orientation_wrt_y = sympy.atan2(z + 1.0e-10, x + 1.0e-10) +v_rbm_y_x = -ra * sympy.sin(orientation_wrt_y) +v_rbm_y_z = ra * sympy.cos(orientation_wrt_y) +v_rbm_y = sympy.Matrix([v_rbm_y_x, 0, v_rbm_y_z]).T + + +# + +# Stokes settings + +stokes.tolerance = 1.0e-3 +stokes.petsc_options["ksp_monitor"] = None + +stokes.petsc_options["snes_type"] = "newtonls" +stokes.petsc_options["ksp_type"] = "fgmres" + +# stokes.petsc_options.setValue("fieldsplit_velocity_pc_type", "mg") +stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_type", "kaskade") +stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_cycle_type", "w") + +stokes.petsc_options["fieldsplit_velocity_mg_coarse_pc_type"] = "svd" + +stokes.petsc_options[f"fieldsplit_velocity_ksp_type"] = "fcg" +stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_type"] = "chebyshev" +stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_max_it"] = 5 +stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_converged_maxits"] = None + +# gasm is super-fast ... but mg seems to be bulletproof +# gamg is toughest wrt viscosity + +stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "gamg") +stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "additive") +stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v") + +# # # mg, multiplicative - very robust ... similar to gamg, additive + +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "mg") +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "multiplicative") +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v") +# thermal buoyancy force + +Gamma = meshball.CoordinateSystem.unit_e_0 +stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) * Gamma, "UpperPlus") + +if not "ball" in grid_type: + stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) * Gamma, "LowerPlus") + +stokes.bodyforce = unit_rvec * Rayleigh * gravity_fn * t_forcing_fn + +# + +timing.reset() +timing.start() + +stokes.solve(zero_init_guess=True) +# + + +# Note: we should remove the rigid body rotation nullspace +# This should be done during the solve, but it is also reasonable to +# remove it from the force terms and solution to prevent it growing if present + +I0 = uw.maths.Integral(meshball, v_rbm_y.dot(v_rbm_y)) +norm = I0.evaluate() +I0.fn = v_soln.sym.dot(v_soln.sym) +vnorm = np.sqrt(I0.evaluate()) + +# for i in range(10): + +I0.fn = v_soln.sym.dot(v_rbm_x) +x_ns = I0.evaluate() / norm +I0.fn = v_soln.sym.dot(v_rbm_y) +y_ns = I0.evaluate() / norm +I0.fn = v_soln.sym.dot(v_rbm_z) +z_ns = I0.evaluate() / norm + +null_space_err = np.sqrt(x_ns**2 + y_ns**2 + z_ns**2) / vnorm + +print( + "Rigid body: {:.4}, {:.4}, {:.4} / {:.4} (x,y,z axis / total)".format( + x_ns, y_ns, z_ns, null_space_err + ) +) +# - +timing.print_table() + +# + +outdir="output" + +meshball.write_timestep( + expt_name, + meshUpdates=True, + meshVars=[p_soln, v_soln], + outputPath=outdir, + index=0, +) + + + +# + +# OR +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_soln.sym) + pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym) + + clipped = pvmesh.clip(origin=(0.0, 0.0, 0.0), normal=(0.0, 1, 0), invert=True) + clipped.point_data["V"] = vis.vector_fn_to_pv_points(clipped, v_soln.sym) + + clippedv = velocity_points.clip(origin=(0.0, 0.0, 0.0), normal=(0.0, 1, 0), invert=True) + clippedv.point_data["V"] = vis.vector_fn_to_pv_points(clippedv, v_soln.sym) + + skip = 7 + points = np.zeros((meshball._centroids[::skip].shape[0], 3)) + points[:, 0] = meshball._centroids[::skip, 0] + points[:, 1] = meshball._centroids[::skip, 1] + points[:, 2] = meshball._centroids[::skip, 2] + point_cloud = pv.PolyData(points) + + pvstream = pvmesh.streamlines_from_source( + point_cloud, vectors="V", + integration_direction="forward", + integrator_type=45, + surface_streamlines=False, + initial_step_length=0.01, + max_time=1.0, + max_steps=1000 + ) + + pl = pv.Plotter(window_size=[1000, 750]) + pl.add_axes() + + pl.add_mesh( + clipped, + cmap="Reds", + # cmap="coolwarm", + edge_color="Black", + show_edges=False, + scalars="T", + use_transparency=False, + show_scalar_bar = False, + opacity=1, + ) + + # pl.add_mesh(pvmesh, cmap="coolwarm", edge_color="Black", show_edges=True, scalars="T", + # use_transparency=False, opacity=1.0) + + + pl.add_mesh(pvstream) + + # arrows = pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], + # show_scalar_bar = False, + # mag=50/Rayleigh, ) + + arrows = pl.add_arrows(clippedv.points, clippedv.point_data["V"], + show_scalar_bar = False, + mag=100/Rayleigh, ) + + # pl.screenshot(filename="sphere.png", window_size=(1000, 1000), return_img=False) + # OR + pl.show(cpos="xy") + +# - + + +pl.screenshot("snapshot.png", window_size=(2000,2000), return_img=False) + +# ! open snapshot.png + + diff --git a/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Swarm_Bubbles.py b/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Swarm_Bubbles.py new file mode 100644 index 0000000..018fc93 --- /dev/null +++ b/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Swarm_Bubbles.py @@ -0,0 +1,383 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Multiple materials - Drips and Blobs +# +# We introduce the notion of an `IndexSwarmVariable` which automatically generates masks for a swarm +# variable that consists of discrete level values (integers). +# +# For a variable $M$, the mask variables are $\left\{ M^0, M^1, M^2 \ldots M^{N-1} \right\}$ where $N$ is the number of indices (e.g. material types) on the variable. This value *must be defined in advance*. +# +# The masks are orthogonal in the sense that $M^i * M^j = 0$ if $i \ne j$, and they are complete in the sense that $\sum_i M^i = 1$ at all points. +# +# The masks are implemented as continuous mesh variables (the user can specify the interpolation order) and so they are also differentiable (once). +# + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import petsc4py +from petsc4py import PETSc + +import underworld3 as uw +from underworld3.systems import Stokes +from underworld3 import function + +import numpy as np +import sympy + +render = True +# - + +meshbox = uw.meshing.UnstructuredSimplexBox( + minCoords=(0.0, 0.0), + maxCoords=(1.0, 1.0), + cellSize=1.0 / 24.0, + regular=True, + qdegree=2, +) + + +# + +# meshbox.quadrature.view() +# - + +meshbox.dm.view() + +# + +# Some useful coordinate stuff + +x, y = meshbox.CoordinateSystem.X + +# - + +v_soln = uw.discretisation.MeshVariable("U", meshbox, meshbox.dim, degree=2) +p_soln = uw.discretisation.MeshVariable("P", meshbox, 1, degree=1) + + +swarm = uw.swarm.Swarm(mesh=meshbox) +material = uw.swarm.IndexSwarmVariable("M", swarm, indices=4, proxy_degree=1) +swarm.populate(fill_param=4) + + +# + +blobs = np.array( + [ + [0.25, 0.75, 0.1, 1], + [0.45, 0.70, 0.05, 2], + [0.65, 0.60, 0.06, 3], + [0.85, 0.40, 0.06, 1], + [0.65, 0.20, 0.06, 2], + [0.45, 0.20, 0.12, 3], + ] +) + + +with swarm.access(material): + material.data[...] = 0 + + for i in range(blobs.shape[0]): + cx, cy, r, m = blobs[i, :] + inside = (swarm.data[:, 0] - cx) ** 2 + (swarm.data[:, 1] - cy) ** 2 < r**2 + material.data[inside] = m + +# - + + +material.sym + +X = meshbox.CoordinateSystem.X + +# + +# The material fields are differentiable + +sympy.derive_by_array(material.sym, X).reshape(2, 4).tomatrix() +# - + +mat_density = np.array([1, 0.1, 0.1, 2]) +density = ( + mat_density[0] * material.sym[0] + + mat_density[1] * material.sym[1] + + mat_density[2] * material.sym[2] + + mat_density[3] * material.sym[3] +) + +# + +mat_viscosity = np.array([1, 0.1, 10.0, 10.0]) +# mat_viscosity = np.array([1, 1, 1.0, 1.0]) + +viscosity = ( + mat_viscosity[0] * material.sym[0] + + mat_viscosity[1] * material.sym[1] + + mat_viscosity[2] * material.sym[2] + + mat_viscosity[3] * material.sym[3] +) +# - + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + points = vis.swarm_to_pv_cloud(swarm) + point_cloud = pv.PolyData(points) + + pvmesh.point_data["M0"] = vis.scalar_fn_to_pv_points(pvmesh, material.sym[0]) + pvmesh.point_data["M1"] = vis.scalar_fn_to_pv_points(pvmesh, material.sym[1]) + pvmesh.point_data["M2"] = vis.scalar_fn_to_pv_points(pvmesh, material.sym[2]) + pvmesh.point_data["M3"] = vis.scalar_fn_to_pv_points(pvmesh, material.sym[3]) + pvmesh.point_data["M"] = (1.0 * pvmesh.point_data["M1"] + + 2.0 * pvmesh.point_data["M2"] + + 3.0 * pvmesh.point_data["M3"]) + pvmesh.point_data["rho"] = vis.scalar_fn_to_pv_points(pvmesh, density) + pvmesh.point_data["visc"] = vis.scalar_fn_to_pv_points(pvmesh, sympy.log(viscosity)) + + + with swarm.access(): + point_cloud.point_data["M"] = material.data.copy() + + pl = pv.Plotter(window_size=(1000, 750)) + + # pl.add_points(point_cloud, color="Black", + # render_points_as_spheres=False, + # point_size=2.5, opacity=0.75) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="visc", + use_transparency=False, + opacity=0.95, + ) + + pl.show(cpos="xy") + +# + +# Create Stokes object + +stokes = uw.systems.Stokes( + meshbox, velocityField=v_soln, pressureField=p_soln, solver_name="stokes" +) + +# Set some things +import sympy +from sympy import Piecewise + +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.viscosity = viscosity + +stokes.bodyforce = sympy.Matrix([0, -density]) +stokes.saddle_preconditioner = 1.0 / viscosity + +# free slip. +# note with petsc we always need to provide a vector of correct cardinality. + +stokes.add_dirichlet_bc((sympy.oo,0.0), "Bottom") +stokes.add_dirichlet_bc((sympy.oo, 0.0), "Top") +stokes.add_dirichlet_bc((0.0,sympy.oo), "Left") +stokes.add_dirichlet_bc((0.0,sympy.oo), "Right") + + +# + +stokes.petsc_options["snes_rtol"] = 1.0e-3 +stokes.petsc_options[ + "snes_atol" +] = 1.0e-5 # Not sure why rtol does not do its job when guess is used + +# stokes.petsc_options["fieldsplit_velocity_ksp_monitor"] = None +# stokes.petsc_options["fieldsplit_pressure_ksp_monitor"] = None +stokes.petsc_options["fieldsplit_velocity_ksp_rtol"] = 1.0e-3 +stokes.petsc_options["fieldsplit_pressure_ksp_rtol"] = 1.0e-2 + + +# - + +stokes.solve(zero_init_guess=True) + +# + +# check the solution + +if uw.mpi.size == 1 and render: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + pvmesh.point_data["rho"] = vis.scalar_fn_to_pv_points(pvmesh, density) + pvmesh.point_data["visc"] = vis.scalar_fn_to_pv_points(pvmesh, sympy.log(viscosity)) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) + pvmesh.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh, v_soln.sym.dot(v_soln.sym)) + + # point sources at cell centres + + cpoints = np.zeros((meshbox._centroids.shape[0] // 4, 3)) + cpoints[:, 0] = meshbox._centroids[::4, 0] + cpoints[:, 1] = meshbox._centroids[::4, 1] + cpoint_cloud = pv.PolyData(cpoints) + + pvstream = pvmesh.streamlines_from_source( + cpoint_cloud, + vectors="V", + integrator_type=45, + integration_direction="forward", + compute_vorticity=False, + max_steps=25, + surface_streamlines=True, + ) + + spoints = vis.swarm_to_pv_cloud(swarm) + spoint_cloud = pv.PolyData(spoints) + + with swarm.access(): + spoint_cloud.point_data["M"] = material.data[...] + + pl = pv.Plotter(window_size=(1000, 750)) + + pl.add_mesh(pvstream, opacity=1) + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Gray", + show_edges=True, + scalars="rho", + opacity=0.5, + ) + + pl.add_points( + spoint_cloud, + cmap="gray_r", + scalars="M", + render_points_as_spheres=True, + point_size=5, + opacity=0.33, + ) + + # pl.add_points(pdata) + + pl.show(cpos="xy") + + +# - + +def plot_mesh(filename): + if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + pvmesh.point_data["rho"] = vis.scalar_fn_to_pv_points(pvmesh, density) + pvmesh.point_data["visc"] = vis.scalar_fn_to_pv_points(pvmesh, sympy.log(viscosity)) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) + pvmesh.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh, v_soln.sym.dot(v_soln.sym)) + + # point sources at cell centres + + cpoints = np.zeros((meshbox._centroids.shape[0], 3)) + cpoints[:, 0] = meshbox._centroids[:, 0] + cpoints[:, 1] = meshbox._centroids[:, 1] + cpoint_cloud = pv.PolyData(cpoints) + + pvstream = pvmesh.streamlines_from_source( + cpoint_cloud, + vectors="V", + integrator_type=45, + integration_direction="forward", + compute_vorticity=False, + max_steps=25, + surface_streamlines=True, + ) + + spoints = vis.swarm_to_pv_cloud(swarm) + spoint_cloud = pv.PolyData(spoints) + + with swarm.access(): + spoint_cloud.point_data["M"] = material.data[...] + + pl = pv.Plotter() + + # pl.add_mesh(pvmesh, "Gray", "wireframe") + # pl.add_arrows(arrow_loc, velocity_field, mag=0.2/vmag, opacity=0.5) + + pl.add_mesh(pvstream, opacity=1) + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Gray", + show_edges=True, + scalars="visc", + opacity=0.5, + ) + + pl.add_points( + spoint_cloud, + cmap="gray_r", + scalars="M", + render_points_as_spheres=True, + point_size=5, + opacity=0.33, + ) + + # pl.add_points(pdata) + + pl.remove_scalar_bar("M") + pl.remove_scalar_bar("visc") + + pl.screenshot( + filename="{}.png".format(filename), + window_size=(1250, 1250), + return_img=False, + ) + + # pl.show() + pv.close_all() + + return + + +t_step = 0 + +# + +# Update in time + +expt_name = "output/blobs" + +for step in range(0, 2): # 250 + stokes.solve(zero_init_guess=False) + delta_t = min(10.0, stokes.estimate_dt()) + + # update swarm / swarm variables + + if uw.mpi.rank == 0: + print("Timestep {}, dt {}".format(t_step, delta_t)) + + # advect swarm + print("Swarm Advection") + swarm.advection(v_soln.fn, delta_t) + print("Swarm Advection - done") + + if t_step % 1 == 0: + plot_mesh(filename="{}_step_{}".format(expt_name, t_step)) + + t_step += 1 + +# - +meshbox.petsc_save_checkpoint(index=step, meshVars=[v_soln], outputPath='./output/') + + diff --git a/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Swarm_RT_Cartesian.py b/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Swarm_RT_Cartesian.py new file mode 100644 index 0000000..9ed89a2 --- /dev/null +++ b/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Swarm_RT_Cartesian.py @@ -0,0 +1,346 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Rayleigh Taylor - swarm materials +# +# We introduce the notion of an `IndexSwarmVariable` which automatically generates masks for a swarm +# variable that consists of discrete level values (integers). +# +# For a variable $M$, the mask variables are $\left\{ M^0, M^1, M^2 \ldots M^{N-1} \right\}$ where $N$ is the number of indices (e.g. material types) on the variable. This value *must be defined in advance*. +# +# The masks are orthogonal in the sense that $M^i * M^j = 0$ if $i \ne j$, and they are complete in the sense that $\sum_i M^i = 1$ at all points. +# +# The masks are implemented as continuous mesh variables (the user can specify the interpolation order) and so they are also differentiable (once). +# + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import petsc4py +from petsc4py import PETSc + +import underworld3 as uw +from underworld3.systems import Stokes +from underworld3 import function + +import numpy as np +import sympy + +render = True + +cell_size = uw.options.getReal("mesh_cell_size", default=1.0 / 32) +particle_fill = uw.options.getInt("particle_fill", default=7) +viscosity_ratio = uw.options.getReal("rt_viscosity_ratio", default=1.0) + + +# + +lightIndex = 0 +denseIndex = 1 + +boxLength = 0.9142 +boxHeight = 1.0 +viscosityRatio = viscosity_ratio +amplitude = 0.02 +offset = 0.2 +model_end_time = 300.0 + +# material perturbation from van Keken et al. 1997 +wavelength = 2.0 * boxLength +k = 2.0 * np.pi / wavelength +# - + +meshbox = uw.meshing.UnstructuredSimplexBox( + minCoords=(0.0, 0.0), + maxCoords=(boxLength, boxHeight), + cellSize=cell_size, + regular=False, + qdegree=2, +) + + +# + +import sympy + +# Some useful coordinate stuff + +x, y = meshbox.CoordinateSystem.X + +# - + +v_soln = uw.discretisation.MeshVariable(r"U", meshbox, meshbox.dim, degree=2) +p_soln = uw.discretisation.MeshVariable(r"P", meshbox, 1, degree=1) +m_cont = uw.discretisation.MeshVariable(r"M_c", meshbox, 1, degree=1, continuous=True) + + +swarm = uw.swarm.Swarm(mesh=meshbox) +material = uw.swarm.IndexSwarmVariable( + r"M", swarm, indices=2, proxy_degree=1, proxy_continuous=False +) +swarm.populate(fill_param=particle_fill) + + +# + +with swarm.access(material): + material.data[...] = 0 + +with swarm.access(material): + perturbation = offset + amplitude * np.cos( + k * swarm.particle_coordinates.data[:, 0] + ) + material.data[:, 0] = np.where( + perturbation > swarm.particle_coordinates.data[:, 1], lightIndex, denseIndex + ) + +material.sym + + +# + +# print(f"Memory usage = {python_process.memory_info().rss//1000000} Mb", flush=True) +# - + + +X = meshbox.CoordinateSystem.X + +mat_density = np.array([0, 1]) # lightIndex, denseIndex +density = mat_density[0] * material.sym[0] + mat_density[1] * material.sym[1] + +mat_viscosity = np.array([viscosityRatio, 1]) +viscosity = mat_viscosity[0] * material.sym[0] + mat_viscosity[1] * material.sym[1] + +# + +# Create Stokes object + +stokes = uw.systems.Stokes( + meshbox, velocityField=v_soln, pressureField=p_soln, solver_name="stokes" +) + +# Set some things +import sympy +from sympy import Piecewise + +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.viscosity = viscosity + +stokes.bodyforce = sympy.Matrix([0, -density]) +stokes.saddle_preconditioner = 1.0 / viscosity + +# free slip. +# note with petsc we always need to provide a vector of correct cardinality. + +stokes.add_dirichlet_bc((sympy.oo,0.0), "Bottom") +stokes.add_dirichlet_bc((sympy.oo, 0.0), "Top") +stokes.add_dirichlet_bc((0.0,sympy.oo), "Left") +stokes.add_dirichlet_bc((0.0,sympy.oo), "Right") +# - + + +stokes.rtol = 1.0e-3 # rough solution is all that's needed + +print("Stokes setup", flush=True) + +m_solver = uw.systems.Projection(meshbox, m_cont) +m_solver.uw_function = material.sym[1] +m_solver.smoothing = 1.0e-3 +m_solver.solve() + +print("Solve projection ... done", flush=True) + +# + +# stokes._setup_terms() +# - + +print("Stokes terms ... done", flush=True) + +stokes.solve(zero_init_guess=True) + +# + +# check the solution + +if uw.mpi.size == 1 and render: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + pvmesh.point_data["rho"] = vis.scalar_fn_to_pv_points(pvmesh, density) + pvmesh.point_data["visc"] = vis.scalar_fn_to_pv_points(pvmesh, sympy.log(viscosity)) + pvmesh.point_data["M"] = vis.scalar_fn_to_pv_points(pvmesh, m_cont.sym) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) + + + # point sources at cell centres + cpoints = np.zeros((meshbox._centroids[::4].shape[0], 3)) + cpoints[:, 0] = meshbox._centroids[::4, 0] + cpoints[:, 1] = meshbox._centroids[::4, 1] + cpoint_cloud = pv.PolyData(cpoints) + + pvstream = pvmesh.streamlines_from_source( + cpoint_cloud, + vectors="V", + integrator_type=45, + integration_direction="forward", + compute_vorticity=False, + max_steps=25, + surface_streamlines=True, + ) + + spoints = vis.swarm_to_pv_cloud(swarm) + spoint_cloud = pv.PolyData(spoints) + + with swarm.access(): + spoint_cloud.point_data["M"] = material.data[...] + + + pl = pv.Plotter(window_size=(1000, 750)) + + pl.add_mesh(pvstream, opacity=1.0) + pl.add_mesh( + pvmesh, + cmap="Blues_r", + edge_color="Gray", + show_edges=True, + scalars="M", + opacity=0.75, + ) + pl.add_points( + spoint_cloud, + cmap="Reds_r", + scalars="M", + render_points_as_spheres=True, + point_size=3, + opacity=0.5, + ) + + # pl.add_points(pdata) + + pl.show(cpos="xy") + + +# + + +def plot_mesh(filename): + if uw.mpi.size == 1: + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + pvmesh.point_data["rho"] = vis.scalar_fn_to_pv_points(pvmesh, density) + pvmesh.point_data["visc"] = vis.scalar_fn_to_pv_points(pvmesh, sympy.log(viscosity)) + pvmesh.point_data["M"] = vis.scalar_fn_to_pv_points(pvmesh, m_cont.sym) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) + + # point sources at cell centres + subsample = 3 + cpoints = np.zeros((meshbox._centroids[::subsample].shape[0], 3)) + cpoints[:, 0] = meshbox._centroids[::subsample, 0] + cpoints[:, 1] = meshbox._centroids[::subsample, 1] + cpoint_cloud = pv.PolyData(cpoints) + + pvstream = pvmesh.streamlines_from_source( + cpoint_cloud, + vectors="V", + integrator_type=45, + integration_direction="forward", + compute_vorticity=False, + max_steps=25, + surface_streamlines=True, + ) + + spoints = vis.swarm_to_pv_cloud(swarm) + spoint_cloud = pv.PolyData(spoints) + + with swarm.access(): + spoint_cloud.point_data["M"] = material.data[...] + + pl.clear() + + # pl.add_mesh(pvmesh, "Gray", "wireframe") + # pl.add_arrows(arrow_loc, velocity_field, mag=0.2/vmag, opacity=0.5) + + pl.add_mesh(pvstream, opacity=1) + pl.add_mesh( + pvmesh, + cmap="Blues_r", + edge_color="Gray", + show_edges=True, + scalars="M", + opacity=0.75, + ) + + pl.add_points( + spoint_cloud, + cmap="Reds_r", + scalars="M", + render_points_as_spheres=True, + point_size=3, + opacity=0.3, + ) + + pl.remove_scalar_bar("M") + pl.remove_scalar_bar("V") + # pl.remove_scalar_bar("rho") + + pl.screenshot( + filename="{}.png".format(filename), + window_size=(1250, 1250), + return_img=False, + ) + + return + + +# - + +t_step = 0 + +# !mkdir output + +# + +# Update in time + +expt_name = "swarm_rt" + +for step in range(0, 2): #250 + stokes.solve(zero_init_guess=False) + m_solver.solve(zero_init_guess=False) + delta_t = min(10.0, stokes.estimate_dt()) + + # update swarm / swarm variables + + if uw.mpi.rank == 0: + print("Timestep {}, dt {}".format(t_step, delta_t)) + + # advect swarm + swarm.advection(v_soln.sym, delta_t) + + if t_step % 5 == 0: + plot_mesh(filename="{}_step_{}".format(expt_name, t_step)) + + # "Checkpoints" + savefile = f"swarm_rt_xy" + + meshbox.write_timestep( + expt_name, + meshUpdates=True, + meshVars=[p_soln, v_soln, m_cont], + outputPath="output", + index=t_step, + ) + + t_step += 1 +# - + + diff --git a/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Swarm_RT_Spherical.py b/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Swarm_RT_Spherical.py new file mode 100644 index 0000000..5109855 --- /dev/null +++ b/main/_sources/Notebooks/Examples-StokesFlow/Ex_Stokes_Swarm_RT_Spherical.py @@ -0,0 +1,377 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Rayleigh-Taylor (Level-set based) in the sphere +# +# If there are just two materials, then an efficient way to manage the interface tracking is through a "level-set" which tracks not just the material type, but the distance to the interface. The distance is a continuous quantity that is not degraded quickly by classical advection schemes. A particle-based level set also has advantages because the smooth signed-distance quantity can be projected to the mesh more accurately than a sharp condition function. + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import os + +os.environ["UW_TIMING_ENABLE"] = "1" + +import petsc4py +from petsc4py import PETSc + +import underworld3 as uw +from underworld3.systems import Stokes +from underworld3 import function +from underworld3 import timing + +import numpy as np +import sympy + +render = True + + +# + +lightIndex = 0 +denseIndex = 1 + +viscosityRatio = 1.0 + +r_layer = 0.7 +r_o = 1.0 +r_i = 0.54 + +res = 0.25 + +Rayleigh = 1.0e6 / (r_o - r_i) ** 3 + +offset = 0.5 * res + + +# + +cell_size = uw.options.getReal("mesh_cell_size", default=0.1) +particle_fill = uw.options.getInt("particle_fill", default=5) +viscosity_ratio = uw.options.getReal("rt_viscosity_ratio", default=1.0) + + +mesh = uw.meshing.SphericalShell( + radiusInner=r_i, radiusOuter=r_o, cellSize=res, qdegree=2 +) + +# - + + +v_soln = uw.discretisation.MeshVariable(r"U", mesh, mesh.dim, degree=2) +p_soln = uw.discretisation.MeshVariable(r"P", mesh, 1, degree=1) +meshr = uw.discretisation.MeshVariable(r"r", mesh, 1, degree=1) + + +swarm = uw.swarm.Swarm(mesh=mesh) +material = uw.swarm.SwarmVariable(r"\cal{L}", swarm, proxy_degree=1, size=1) +swarm.populate(fill_param=2) + + +with swarm.access(material): + r = np.sqrt( + swarm.particle_coordinates.data[:, 0] ** 2 + + swarm.particle_coordinates.data[:, 1] ** 2 + + (swarm.particle_coordinates.data[:, 2] - offset) ** 2 + ) + + material.data[:, 0] = r - r_layer + +# + + +# Some useful coordinate stuff + +x, y, z = mesh.CoordinateSystem.X +ra, l1, l2 = mesh.CoordinateSystem.xR + + +# + + +density = sympy.Piecewise((0.0, material.sym[0] < 0.0), (1.0, True)) +display(density) + +viscosity = sympy.Piecewise((1.0, material.sym[0] < 0.0), (1.0, True)) +display(viscosity) + +# - + +with swarm.access(): + print(material.data.max(), material.data.min()) + +if False: + import numpy as np + import pyvista as pv + import vtk + + pv.global_theme.background = "white" + pv.global_theme.window_size = [750, 750] + pv.global_theme.antialiasing = True + pv.global_theme.jupyter_backend = "panel" + pv.global_theme.smooth_shading = True + + mesh.vtk("tmp_mesh.vtk") + pvmesh = pv.read("tmp_mesh.vtk") + + with swarm.access(): + points = np.zeros((swarm.data.shape[0], 3)) + points[:, 0] = swarm.data[:, 0] + points[:, 1] = swarm.data[:, 1] + points[:, 2] = swarm.data[:, 2] + + point_cloud = pv.PolyData(points) + + with mesh.access(): + pvmesh.point_data["M"] = uw.function.evaluate(material.sym[0], mesh.data) + pvmesh.point_data["rho"] = uw.function.evaluate(density, mesh.data) + pvmesh.point_data["visc"] = uw.function.evaluate( + sympy.log(viscosity), mesh.data + ) + + with swarm.access(): + point_cloud.point_data["M"] = material.data.copy() + + pl = pv.Plotter() + + pl.add_mesh(pvmesh, "Black", "wireframe") + + pl.add_points( + point_cloud, + cmap="coolwarm", + scalars="M", + render_points_as_spheres=True, + point_size=2, + opacity=0.5, + ) + + # pl.add_mesh( + # pvmesh, + # cmap="coolwarm", + # edge_color="Black", + # show_edges=True, + # scalars="M1", + # use_transparency=False, + # opacity=0.25, + # ) + + pl.show(cpos="xy") + + +# + +stokes = uw.systems.Stokes( + mesh, + velocityField=v_soln, + pressureField=p_soln, + verbose=False, + solver_name="stokes", +) + +# stokes.petsc_options.delValue("ksp_monitor") # We can flip the default behaviour at some point +stokes.petsc_options["snes_rtol"] = 1.0e-4 +stokes.petsc_options["snes_rtol"] = 1.0e-3 +stokes.petsc_options["ksp_monitor"] = None + +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.viscosity = viscosity + +Gamma = mesh.Gamma +stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) * Gamma, "Upper") +stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) * Gamma, "Lower") + +# buoyancy (magnitude) +buoyancy = Rayleigh * density # * (1 - surface_fn) * (1 - base_fn) + +unit_vec_r = mesh.CoordinateSystem.X / mesh.CoordinateSystem.xR[0] + +# Free slip condition by penalizing radial velocity at the surface (non-linear term) + +stokes.bodyforce = -unit_vec_r * buoyancy + +stokes.saddle_preconditioner = 1 / viscosity + +# - + +mesh.CoordinateSystem.unit_e_0.shape +(mesh.CoordinateSystem.X / mesh.CoordinateSystem.xR[0]).shape + +with mesh.access(meshr): + meshr.data[:, 0] = uw.function.evaluate( + sympy.sqrt(x**2 + y**2 + z**2), mesh.data, mesh.N + ) # cf radius_fn which is 0->1 + + +# + +timing.reset() +timing.start() + +stokes.solve(zero_init_guess=True) + +timing.print_table() + +# + +# check the solution + +if uw.mpi.size == 1 and render: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh) + pvmesh.point_data["rho"] = vis.scalar_fn_to_pv_points(pvmesh, density) + pvmesh.point_data["M"] = vis.scalar_fn_to_pv_points(pvmesh, material.sym) + pvmesh.point_data["V"] = 10.0 * vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)/vis.vector_fn_to_pv_points(pvmesh, v_soln.sym).max() + + # point sources at cell centres + + subsample = 2 + + cpoints = np.zeros((mesh._centroids[::subsample, 0].shape[0], 3)) + cpoints[:, 0] = mesh._centroids[::subsample, 0] + cpoints[:, 1] = mesh._centroids[::subsample, 1] + cpoints[:, 2] = mesh._centroids[::subsample, 2] + + cpoint_cloud = pv.PolyData(cpoints) + + pvstream = pvmesh.streamlines_from_source( + cpoint_cloud, + vectors="V", + integrator_type=45, + integration_direction="both", + compute_vorticity=False, + surface_streamlines=False, + ) + + spoints = vis.swarm_to_pv_cloud(swarm) + spoint_cloud = pv.PolyData(spoints) + + with swarm.access(): + spoint_cloud.point_data["M"] = material.data[...] + + contours = pvmesh.contour(isosurfaces=[0.0], scalars="M") + + pl = pv.Plotter(window_size=(1000, 1000)) + + pl.add_mesh(pvmesh, "Black", "wireframe", opacity=0.5) + # pl.add_arrows(arrow_loc, velocity_field, mag=0.2/vmag, opacity=0.5) + + pl.add_mesh(pvstream, opacity=1.0, cmap="RdGy_r",) + # pl.add_mesh(pvmesh, cmap="Blues_r", edge_color="Gray", show_edges=True, scalars="rho", opacity=0.25) + + # pl.add_mesh(contours, opacity=1, color="Blue") + + pl.add_points(spoint_cloud, cmap="Reds_r", scalars="M", render_points_as_spheres=True, point_size=10, opacity=0.3) + # pl.add_points(pdata) + + pl.show(cpos="xz") + + +# + + +def plot_mesh(filename): + if uw.mpi.size != 1: + return + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh) + pvmesh.point_data["rho"] = vis.scalar_fn_to_pv_points(pvmesh, density) + pvmesh.point_data["M"] = vis.scalar_fn_to_pv_points(pvmesh, material.sym) + pvmesh.point_data["V"] = 10.0 * vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)/vis.vector_fn_to_pv_points(pvmesh, v_soln.sym).max() + print(f"Vscale {vis.vector_fn_to_pv_points(pvmesh, v_soln.sym).max()}") + + # point sources at cell centres + + cpoints = np.zeros((mesh._centroids[::2].shape[0], 3)) + cpoints[:, 0] = mesh._centroids[::2, 0] + cpoints[:, 1] = mesh._centroids[::2, 1] + cpoint_cloud = pv.PolyData(cpoints) + + pvstream = pvmesh.streamlines_from_source( + cpoint_cloud, + vectors="V", + integrator_type=45, + integration_direction="both", + compute_vorticity=False, + surface_streamlines=False, + ) + + spoints = vis.swarm_to_pv_cloud(swarm) + spoint_cloud = pv.PolyData(spoints) + + with swarm.access(): + spoint_cloud.point_data["M"] = material.data[...] + + contours = pvmesh.contour(isosurfaces=[0.0], scalars="M") + + ## Plotting into existing pl (memory leak in pyvista) + pl.clear() + + pl.add_mesh(pvmesh, "Gray", "wireframe") + # pl.add_arrows(arrow_loc, velocity_field, mag=0.2/vmag, opacity=0.5) + + pl.add_mesh(pvstream, opacity=0.33) + # pl.add_mesh(pvmesh, cmap="Blues_r", edge_color="Gray", show_edges=True, scalars="rho", opacity=0.25) + + # pl.add_points( + # spoint_cloud, cmap="Reds_r", scalars="M", render_points_as_spheres=True, point_size=2, opacity=0.3 + # ) + + pl.add_mesh(contours, opacity=0.75, color="Yellow") + + # pl.remove_scalar_bar("Mat") + pl.remove_scalar_bar("V") + # pl.remove_scalar_bar("rho") + + pl.camera_position = "xz" + pl.screenshot( + filename="{}.png".format(filename), + window_size=(1000, 1000), + return_img=False, + ) + + return + + +# - + +t_step = 0 + +# + +# Update in time + +expt_name = "output/swarm_rt_sph" + +for step in range(0, 10): + stokes.solve(zero_init_guess=False) + delta_t = 2.0 * stokes.estimate_dt() + + # update swarm / swarm variables + + if uw.mpi.rank == 0: + print("Timestep {}, dt {}".format(t_step, delta_t)) + + # advect swarm + swarm.advection(v_soln.sym, delta_t) + + if t_step < 10 or t_step % 5 == 0: + # plot_mesh(filename="{}_step_{}".format(expt_name, t_step)) + + mesh.petsc_save_checkpoint(index=t_step, meshVars=[v_soln], outputPath='./output/') + + t_step += 1 + +# - + + + diff --git a/main/_sources/Notebooks/Examples-StokesFlow/Ex_WIP_Stokes_Periodic.py b/main/_sources/Notebooks/Examples-StokesFlow/Ex_WIP_Stokes_Periodic.py new file mode 100644 index 0000000..02b171d --- /dev/null +++ b/main/_sources/Notebooks/Examples-StokesFlow/Ex_WIP_Stokes_Periodic.py @@ -0,0 +1,214 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() +# + [markdown] magic_args="[markdown]" +# # Periodic Mesh Example (WIP) +# +# This is a periodic, Cartesian mesh with the periodic bcs specified using gmsh itself. +# Compare this to the Cylindrical Stokes example that has periodic coordinates in a mesh +# that is continuously connected. + +# + [markdown] magic_args="[markdown]" +# ## Generate Periodic mesh using GMSH +# - + +# %% +import gmsh + +gmsh.initialize() + +# %% +gmsh.model.add("Periodic x") + +# %% +minCoords = (0.0, 0.0) +maxCoords = (1.0, 1.0) +cellSize = 0.1 + +# %% +boundaries = { + "Bottom": 1, + "Top": 2, + "Right": 3, + "Left": 4, +} + +# %% +xmin, ymin = minCoords +xmax, ymax = maxCoords + +# %% +p1 = gmsh.model.geo.add_point(xmin, ymin, 0.0, meshSize=cellSize) +p2 = gmsh.model.geo.add_point(xmax, ymin, 0.0, meshSize=cellSize) +p3 = gmsh.model.geo.add_point(xmin, ymax, 0.0, meshSize=cellSize) +p4 = gmsh.model.geo.add_point(xmax, ymax, 0.0, meshSize=cellSize) + +l1 = gmsh.model.geo.add_line(p1, p2, tag=boundaries["Bottom"]) +l2 = gmsh.model.geo.add_line(p2, p4, tag=boundaries["Right"]) +l3 = gmsh.model.geo.add_line(p4, p3, tag=boundaries["Top"]) +l4 = gmsh.model.geo.add_line(p3, p1, tag=boundaries["Left"]) + +cl = gmsh.model.geo.add_curve_loop((l1, l2, l3, l4)) +surface = gmsh.model.geo.add_plane_surface([cl]) + +# %% +gmsh.model.geo.synchronize() + +# %% +translation = [1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] + +# %% +gmsh.model.mesh.setPeriodic(1, [boundaries["Right"]], [boundaries["Left"]], translation) + +# %% +# Add Physical groups +for name, tag in boundaries.items(): + gmsh.model.add_physical_group(1, [tag], tag) + gmsh.model.set_physical_name(1, tag, name) + +gmsh.model.addPhysicalGroup(2, [surface], surface) +gmsh.model.setPhysicalName(2, surface, "Elements") + +# %% +gmsh.model.mesh.generate(2) +gmsh.write("tmp_periodicx.msh") +gmsh.finalize() + +# + [markdown] magic_args="[markdown]" +# ## Import Mesh into PETSc +# - + +# %% +from petsc4py import PETSc +import underworld3 as uw +from underworld3.systems import Stokes +import numpy as np + +options = PETSc.Options() + + +# %% +plex = PETSc.DMPlex().createFromFile("tmp_periodicx.msh") + +# %% +for name, tag in boundaries.items(): + plex.createLabel(name) + label = plex.getLabel(name) + indexSet = plex.getStratumIS("Face Sets", tag) + if indexSet: + label.insertIS(indexSet, 1) + else: + plex.removeLabel(name) + +plex.removeLabel("Face Sets") + +# %% +plex.view() + +# %% +from underworld3.discretisation import Mesh + +# %% +mesh = Mesh(plex, degree=1) + +# %% +mesh.dm.view() + +# %% +swarm = uw.swarm.Swarm(mesh=mesh) +material = uw.swarm.IndexSwarmVariable("M", swarm, indices=2, proxy_degree=1) +swarm.populate(fill_param=3) + +# Create Stokes object + +v = uw.discretisation.MeshVariable("U", mesh, mesh.dim, degree=2) +p = uw.discretisation.MeshVariable("P", mesh, 1, degree=1) + +stokes = uw.systems.Stokes(mesh, velocityField=v, pressureField=p) +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.viscosity = 1 + +# No slip boundary conditions +stokes.add_dirichlet_bc((0.5, 0.0), "Top", (0, 1)) +stokes.add_dirichlet_bc((-0.5, 0.0), "Bottom", (0, 1)) + +# %% +# Write density into a variable for saving +densvar = uw.discretisation.MeshVariable("density", mesh, 1) +with mesh.access(densvar): + densvar.data[:, 0] = 1.0 + +# %% +swarm.dm.getCoordinates().array + +# %% +# body force +import sympy + +x, y = mesh.X + +unit_rvec = mesh.rvec / sympy.sqrt(mesh.rvec.dot(mesh.rvec)) +stokes.bodyforce = 0 * mesh.X # -mesh.X / sympy.sqrt(x**2 + y**2) + +# %% +# Solve time +stokes.solve() + +mesh.petsc_save_checkpoint(index=0, meshVars=[stokes.u, stokes.p, densvar], outputPath='./output/') +swarm.petsc_save_checkpoint(swarmName='swarm', index=0, outputPath='./output/') + +# check if that works + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh("tmp_periodicx.msh") + # pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(v) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v.sym) + + pl = pv.Plotter(window_size=(1000, 750)) + + pl.add_mesh(pvmesh, "Black", "wireframe") + # pl.add_arrows(pvmesh.points, pvmesh.point_data["V"], mag=5.0e-1, opacity=0.5) + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=5.0e-1, opacity=0.5) + + pl.show(cpos="xy") + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh("tmp_periodicx.msh") + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v.sym) + + # velocity_points = vis.meshVariable_to_pv_cloud(v) + # velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v.sym) + + pl = pv.Plotter(window_size=(1000, 750)) + + pl.add_mesh(pvmesh, "Black", "wireframe") + pl.add_arrows(pvmesh.points, pvmesh.point_data["V"], mag=5.0e-1, opacity=0.5) + # pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=5.0e-1, opacity=0.5) + + pl.show(cpos="xy") + + diff --git a/main/_sources/Notebooks/Examples-StokesFlow/Ex_stokes_sinkingBlock_benchmark.py b/main/_sources/Notebooks/Examples-StokesFlow/Ex_stokes_sinkingBlock_benchmark.py new file mode 100644 index 0000000..469dd88 --- /dev/null +++ b/main/_sources/Notebooks/Examples-StokesFlow/Ex_stokes_sinkingBlock_benchmark.py @@ -0,0 +1,431 @@ +# %% [markdown] +# # Stokes sinker - sinking block +# +# Sinking block benchmark as outlined in: +# +# - [Gerya, T.V. and Yuen, D.A., 2003. Characteristics-based marker-in-cell method with conservative finite-differences schemes for modeling geological flows with strongly variable transport properties. Physics of the Earth and Planetary Interiors, 140(4), pp.293-318.](http://jupiter.ethz.ch/~tgerya/reprints/2003_PEPI_method.pdf) +# +# - Only value to change is: **viscBlock** +# +# - utilises the UW scaling module to convert from dimensional to non-dimensional values +# +# - Includes a passive tracer that is handled by UW swarm routines (and should be parallel safe). + +# %% +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# %% +from petsc4py import PETSc +import underworld3 as uw +import numpy as np +import sympy + +options = PETSc.Options() + + +# %% +sys = PETSc.Sys() +sys.pushErrorHandler("traceback") + +options["snes_converged_reason"] = None +options["snes_monitor_short"] = None + + +# %% +# import unit registry to make it easy to convert between units +u = uw.scaling.units + +### make scaling easier +ndim = uw.scaling.non_dimensionalise +dim = uw.scaling.dimensionalise + +# %% +# Set the resolution, a structured quad box of 51x51 is used in the paper +# res = 51 +res = 21 # use lower res for testing + +nsteps = 1 # number of time steps +swarmGPC = 2 # swarm fill parameter +render = True # plot images + +# %% +refLength = 500e3 +refDensity = 3.3e3 +refGravity = 9.81 +refVelocity = (1 * u.centimeter / u.year).to(u.meter / u.second).m ### 1 cm/yr in m/s +refViscosity = 1e21 +refPressure = refDensity * refGravity * refLength +refTime = refViscosity / refPressure + +bodyforce = ( + refDensity * u.kilogram / u.metre**3 * refGravity * u.meter / u.second**2 +) + +# %% +KL = refLength * u.meter +Kt = refTime * u.second +KM = bodyforce * KL**2 * Kt**2 + +scaling_coefficients = uw.scaling.get_coefficients() +scaling_coefficients["[length]"] = KL +scaling_coefficients["[time]"] = Kt +scaling_coefficients["[mass]"] = KM +scaling_coefficients + +# %% +### fundamental values +ref_length = uw.scaling.dimensionalise(1.0, u.meter).magnitude +ref_length_km = uw.scaling.dimensionalise(1.0, u.kilometer).magnitude +ref_density = uw.scaling.dimensionalise(1.0, u.kilogram / u.meter**3).magnitude +ref_gravity = uw.scaling.dimensionalise(1.0, u.meter / u.second**2).magnitude +ref_temp = uw.scaling.dimensionalise(1.0, u.kelvin).magnitude +ref_velocity = uw.scaling.dimensionalise(1.0, u.meter / u.second).magnitude + +### derived values +ref_time = ref_length / ref_velocity +ref_time_Myr = dim(1, u.megayear).m +ref_pressure = ref_density * ref_gravity * ref_length +ref_stress = ref_pressure +ref_viscosity = ref_pressure * ref_time + +### Key ND values +ND_gravity = 9.81 / ref_gravity + +# %% +# define some names for our index +materialLightIndex = 0 +materialHeavyIndex = 1 + +## Set constants for the viscosity and density of the sinker. +viscBG = 1e21 / ref_viscosity +viscBlock = 1e21 / ref_viscosity + +## set density of blocks +densityBG = 3.2e3 / ref_density +densityBlock = 3.3e3 / ref_density + +# %% +xmin, xmax = 0.0, ndim(500 * u.kilometer) +ymin, ymax = 0.0, ndim(500 * u.kilometer) + +# %% +xmin, xmax + +# %% +# Set the box min and max coords +boxCentre_x, boxCentre_y = ndim(250.0 * u.kilometer), ndim(375.0 * u.kilometer) + +box_xmin, box_xmax = boxCentre_x - ndim(50 * u.kilometer), boxCentre_x + ndim( + 50 * u.kilometer +) +box_ymin, box_ymax = boxCentre_y - ndim(50 * u.kilometer), boxCentre_y + ndim( + 50 * u.kilometer +) + +# location of tracer at bottom of sinker +x_pos = box_xmax - ((box_xmax - box_xmin) / 2.0) +y_pos = box_ymin + +# %% +# mesh = uw.meshing.UnstructuredSimplexBox(minCoords=(0.0,0.0), +# maxCoords=(1.0,1.0), +# cellSize=1.0/res, +# regular=True) + +# mesh = uw.meshing.UnstructuredSimplexBox(minCoords=(xmin, ymin), maxCoords=(xmax, ymax), cellSize=1.0 / res, regular=False) + +mesh = uw.meshing.StructuredQuadBox( + elementRes=(int(res), int(res)), minCoords=(xmin, ymin), maxCoords=(xmax, ymax) +) + + +# %% +### Create Stokes object + +v = uw.discretisation.MeshVariable("U", mesh, mesh.dim, degree=2) +p = uw.discretisation.MeshVariable("P", mesh, 1, degree=1) + +stokes = uw.systems.Stokes(mesh, velocityField=v, pressureField=p) +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel + +# %% +#### No slip +sol_vel = sympy.Matrix([0.0, 0.0]) + +### free slip +stokes.add_dirichlet_bc(sol_vel, "Left", 0) # left/right: components, function, markers +stokes.add_dirichlet_bc(sol_vel, "Right", 0) # left/right: components, function, markers +stokes.add_dirichlet_bc(sol_vel, "Top", 1) # left/right: components, function, markers +stokes.add_dirichlet_bc(sol_vel, "Bottom", 1) # left/right: components, function, markers + + +# %% +## Solver + +stokes.petsc_options["snes_type"] = "newtonls" +stokes.petsc_options["ksp_type"] = "fgmres" + +stokes.petsc_options["snes_monitor"] = None +stokes.petsc_options["ksp_monitor"] = None + +# stokes.petsc_options.setValue("fieldsplit_velocity_pc_type", "mg") +stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_type", "kaskade") +stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_cycle_type", "w") + +stokes.petsc_options["fieldsplit_velocity_mg_coarse_pc_type"] = "svd" +stokes.petsc_options[f"fieldsplit_velocity_ksp_type"] = "fcg" +stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_type"] = "chebyshev" +stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_max_it"] = 7 +stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_converged_maxits"] = None + +# gasm is super-fast ... but mg seems to be bulletproof +# gamg is toughest wrt viscosity + +stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "gamg") +stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "additive") +stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v") + +# # # mg, multiplicative - very robust ... similar to gamg, additive + +stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "mg") +stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "multiplicative") +stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v") + + +# %% +swarm = uw.swarm.Swarm(mesh=mesh) +material = uw.swarm.IndexSwarmVariable("M", swarm, indices=2) +swarm.populate(fill_param=swarmGPC) + +# %% +with swarm.access(material): + material.data[...] = materialLightIndex + material.data[ + (swarm.data[:, 0] >= box_xmin) + & (swarm.data[:, 0] <= box_xmax) + & (swarm.data[:, 1] >= box_ymin) + & (swarm.data[:, 1] <= box_ymax) + ] = materialHeavyIndex + + +# %% +### add tracer for sinker velocity +tracer = np.zeros(shape=(1, 2)) +tracer[:, 0], tracer[:, 1] = x_pos, y_pos + +# %% +passiveSwarm = uw.swarm.Swarm(mesh) +passiveSwarm.dm.finalizeFieldRegister() +passiveSwarm.dm.addNPoints(npoints=len(tracer)) +passiveSwarm.dm.setPointCoordinates(tracer) + +# %% +mat_density = np.array([densityBG, densityBlock]) + +density = mat_density[0] * material.sym[0] + mat_density[1] * material.sym[1] + +# %% +mat_viscosity = np.array([viscBG, viscBlock]) + +viscosityMat = mat_viscosity[0] * material.sym[0] + mat_viscosity[1] * material.sym[1] + + +# %% +def plot_mat(): + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh) + points = vis.swarm_to_pv_cloud(swarm) + + point_cloud = pv.PolyData(points) + with swarm.access(): + point_cloud.point_data["M"] = material.data.copy() + + pl = pv.Plotter(notebook=True) + + pl.add_mesh(pvmesh, "Black", "wireframe") + + pl.add_mesh( + point_cloud, + cmap="coolwarm", + edge_color="Black", + show_edges=False, + scalars="M", + use_transparency=False, + opacity=0.95, + ) + + pl.show(cpos="xy") + + +if render and uw.mpi.size == 1: + plot_mat() + +# %% +### linear solve +stokes.constitutive_model.Parameters.shear_viscosity_0 = ndim( + ref_viscosity * u.pascal * u.second +) +stokes.bodyforce = sympy.Matrix([0, -1 * ND_gravity * density]) + + +# %% +stokes.solve() + +# %% +### add in material-based viscosity +stokes.constitutive_model.Parameters.viscosity = viscosityMat + +# %% +# stokes.petsc_options.view() +options["snes_converged_reason"] = None +options["snes_monitor_short"] = None +options["snes_test_jacobian"] = None +options["snes_test_jacobian_view"] = None +# stokes.petsc_options['snes_test_jacobian'] = None +# stokes.petsc_options['snes_test_jacobian_view'] = None + +# %% +stokes.bodyforce + +# %% +stokes.constitutive_model.Parameters.viscosity + +# %% +step = 0 +time = 0.0 +tSinker = np.zeros(nsteps + 1) * np.nan +ySinker = np.zeros(nsteps + 1) * np.nan + + +# %% +def record_tracer(step, time): + ### Get the position of the sinking ball + with passiveSwarm.access(passiveSwarm): + if passiveSwarm.dm.getLocalSize() > 0: + ymin = passiveSwarm.data[:, 1].min() + ySinker[step] = ymin + tSinker[step] = time + + ### print some stuff + if passiveSwarm.dm.getLocalSize() > 0: + print( + f"Step: {str(step).rjust(3)}, time: {dim(time, u.megayear).m:6.2f} [Myr], tracer: {dim(ymin, u.kilometer):6.2f} [km]" + ) + + +record_tracer(step, time) + +while step < nsteps: + ### solve stokes + stokes.solve() + ### estimate dt + dt = 0.5 * stokes.estimate_dt() + + ### advect the swarm + swarm.advection(stokes.u.sym, dt, corrector=False) + passiveSwarm.advection(stokes.u.sym, dt, corrector=False) + + ### advect tracer + # vel_on_tracer = uw.function.evaluate(stokes.u.fn,tracer) + # tracer += dt*vel_on_tracer + step += 1 + time += dt + + record_tracer(step, time) + +# %% +if passiveSwarm.dm.getLocalSize() > 0: + import matplotlib.pyplot as plt + + ### remove nan values, if any. Convert to km and Myr + ySinker = dim(ySinker[~np.isnan(ySinker)], u.kilometer) + tSinker = dim(tSinker[~np.isnan(tSinker)], u.megayear) + + print("Initial position: t = {0:.3f}, y = {1:.3f}".format(tSinker[0], ySinker[0])) + print("Final position: t = {0:.3f}, y = {1:.3f}".format(tSinker[-1], ySinker[-1])) + + UWvelocity = ( + ((ySinker[0] - ySinker[-1]) / (tSinker[-1] - tSinker[0])) + .to(u.meter / u.second) + .m + ) + print(f"Velocity: v = {UWvelocity} m/s") + + if uw.mpi.size == 0: + fig = plt.figure() + fig.set_size_inches(12, 6) + ax = fig.add_subplot(1, 1, 1) + ax.plot(tSinker.m, ySinker.m) + ax.set_xlabel("Time [Myr]") + ax.set_ylabel("Sinker position [km]") + +# %% [markdown] +# ##### compare values against published results +# +# +# - The marker, representing the velocity calculated from the UW model, should fit along the curved line. +# - These velocities are taken from _Gerya (2010), Introduction to numerical modelling (2nd Ed), page 345_, but show the same model referenced in the paper above + +# %% +from scipy.interpolate import interp1d + + +#### col 0 is log10( visc_block / visc_BG ), col 1 is block velocity, m/s + +paperData = np.array( + [ + (-6.01810758939326, 1.3776991077026654e-9), + (-5.014458950015076, 1.3792676876049961e-9), + (-4.018123543216514, 1.3794412652019993e-9), + (-3.021737084183539, 1.3740399388011341e-9), + (-2.0104944249364634, 1.346341549020515e-9), + (-1.0053652707603105, 1.1862379129846573e-9), + (-0.005609364256097038, 8.128929227244664e-10), + (0.993865754958847, 4.702099044525527e-10), + (2.005950776073732, 3.505255987071023e-10), + (3.0024521026341358, 3.3258073831103253e-10), + (4.006139031188129, 3.2996814021496194e-10), + (5.00247443798669, 3.301417178119651e-10), + (6.013474599120308, 3.289241220212219e-10), + ] +) + +### some errors from sampling, so rounding are used +visc_ratio = np.round(paperData[:, 0]) +paperVelocity = np.round(paperData[:, 1], 11) + +f = interp1d(visc_ratio, paperVelocity, kind="cubic") +x = np.arange(-6, 6, 0.01) + +if uw.mpi.size == 1 and render: + import matplotlib.pyplot as plt + + plt.title("check benchmark") + plt.plot(x, f(x), label="benchmark velocity curve", c="k") + plt.scatter( + np.log10(viscBlock / viscBG), + UWvelocity, + label="model velocity", + c="red", + marker="x", + ) + plt.legend() + + +# %% +### for uw testing system +def test_sinkBlock(): + if uw.mpi.size == 1: + assert np.isclose(f(0.0), UWvelocity) + + +# %% +if render and uw.mpi.size == 1: + plot_mat() + +# %% diff --git a/main/_sources/Notebooks/Examples-StokesFlow/Readme.md b/main/_sources/Notebooks/Examples-StokesFlow/Readme.md new file mode 100644 index 0000000..5b136d8 --- /dev/null +++ b/main/_sources/Notebooks/Examples-StokesFlow/Readme.md @@ -0,0 +1,27 @@ +# Stokes Flow + + +```python + +``` + +## Recent solver and visualization updates + +- [ ] Ex_Stokes_Cartesian_SolNL.py + - [ ] Some issue with clang compiler. Compiler not able find the analytical solution file. +- [x] Ex_Stokes_Disk_Cartesian-FixedStars.py +- [x] Ex_Stokes_Disk_Cartesian.py +- [x] Ex_Stokes_Disk_CylCoords.py +- [x] Ex_Stokes_Sinker.py +- [ ] Ex_Stokes_Sphere_SphCoords_WIP.py + - [ ] UW evaluate issue inside plotting +- [x] Ex_Stokes_Spherical_FreeSlipBCs.py +- [x] Ex_Stokes_Swarm_Bubbles.py +- [x] Ex_Stokes_Swarm_RT_Cartesian.py +- [x] Ex_stokes_sinkingBlock_benchmark.py +- [x] ISSUE_Function_Evaluation_mem_leak.py +- [x] Untitled.ipynb +- [x] StokesIC_FreeSlip_DOCS.py +- [x] Ex_WIP_Stokes_Periodic.py +- [x] Ex_Stokes_Swarm_RT_Spherical.py +- [x] Ex_Stokes_Cartesian_SolC.py \ No newline at end of file diff --git a/main/_sources/Notebooks/Examples-StokesFlow/output/README.md b/main/_sources/Notebooks/Examples-StokesFlow/output/README.md new file mode 100644 index 0000000..8448444 --- /dev/null +++ b/main/_sources/Notebooks/Examples-StokesFlow/output/README.md @@ -0,0 +1,3 @@ +# Convection model outputs + +These files are NOT under version control diff --git a/main/_sources/Notebooks/Examples-Stokes_Kernels/Ex_Stokes_Annulus_Kernels.py b/main/_sources/Notebooks/Examples-Stokes_Kernels/Ex_Stokes_Annulus_Kernels.py new file mode 100644 index 0000000..b60cf9a --- /dev/null +++ b/main/_sources/Notebooks/Examples-Stokes_Kernels/Ex_Stokes_Annulus_Kernels.py @@ -0,0 +1,290 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Cylindrical Stokes +# +# Mesh with embedded internal surface. This allows us to introduce an internal force integral +# + +# + language="html" +# +# +# +# - + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import petsc4py +from petsc4py import PETSc + +import underworld3 as uw +from underworld3.systems import Stokes +from underworld3 import function + +import numpy as np +import sympy + +res = 0.05 +r_o = 1.0 +r_int = 0.8 +r_i = 0.5 + +free_slip_upper = True +free_slip_lower = True + +options = PETSc.Options() +# options["help"] = None +# options["pc_type"] = "svd" +# options["dm_plex_check_all"] = None + +import os + +os.environ["SYMPY_USE_CACHE"] = "no" +# - + + + +meshball = uw.meshing.AnnulusInternalBoundary(radiusOuter=r_o, + radiusInternal=r_int, + radiusInner=r_i, + cellSize_Inner=res, + cellSize_Internal=res*0.5, + cellSize_Outer=res, + filename="tmp_fixedstarsMesh.msh") + + +# + +norm_v = uw.discretisation.MeshVariable("N", meshball, 2, degree=1, varsymbol=r"{\hat{n}}") + +projection = uw.systems.Vector_Projection(meshball, norm_v) +projection.uw_function = sympy.Matrix([[0,0]]) +projection.smoothing = 1.0e-3 + +# Point in a consistent direction wrt vertical +GammaNorm = meshball.Gamma.dot(meshball.CoordinateSystem.unit_e_0) / sympy.sqrt(meshball.Gamma.dot(meshball.Gamma)) + +projection.add_natural_bc(meshball.Gamma * GammaNorm, "Upper") +projection.add_natural_bc(meshball.Gamma * GammaNorm, "Lower") +projection.add_natural_bc(meshball.Gamma * GammaNorm, "Internal") + +projection.solve(verbose=False, debug=False) + +with meshball.access(norm_v): + norm_v.data[:,:] /= np.sqrt(norm_v.data[:,0]**2 + norm_v.data[:,1]**2).reshape(-1,1) + + + +# + +# Create a density structure / buoyancy force +# gravity will vary linearly from zero at the centre +# of the sphere to (say) 1 at the surface + +radius_fn = meshball.CoordinateSystem.xR[0] +unit_rvec = meshball.CoordinateSystem.unit_e_0 +gravity_fn = 1 # radius_fn / r_o + +# Some useful coordinate stuff + +x, y = meshball.CoordinateSystem.X +r, th = meshball.CoordinateSystem.xR + +# Null space in velocity (constant v_theta) expressed in x,y coordinates +v_theta_fn_xy = r * meshball.CoordinateSystem.rRotN.T * sympy.Matrix((0,1)) + +Rayleigh = 1.0e5 +# - +v_soln = uw.discretisation.MeshVariable("V0", meshball, 2, degree=2, varsymbol=r"{v_0}") +v_soln1 = uw.discretisation.MeshVariable("V1", meshball, 2, degree=2, varsymbol=r"{v_1}") +p_soln = uw.discretisation.MeshVariable("p", meshball, 1, degree=1, continuous=True) +p_cont = uw.discretisation.MeshVariable("pc", meshball, 1, degree=1, continuous=True) + +# + +# Create Stokes object + +stokes = Stokes( + meshball, velocityField=v_soln, pressureField=p_soln, solver_name="stokes" +) + +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.viscosity = 1.0 +stokes.penalty = 0.0 +stokes.saddle_preconditioner = sympy.simplify(1 / (stokes.constitutive_model.viscosity + stokes.penalty)) + +stokes.petsc_options.setValue("ksp_monitor", None) +stokes.petsc_options.setValue("snes_monitor", None) +stokes.petsc_options["fieldsplit_velocity_mg_coarse_pc_type"] = "svd" + +t_init = sympy.sin(5*th) * sympy.exp(-1000.0 * ((r - r_int) ** 2)) + + +# + +## First solve with known normals + +stokes.bodyforce = sympy.Matrix([0,0]) +Gamma = meshball.CoordinateSystem.unit_e_0 + +stokes.add_natural_bc(-t_init * unit_rvec, "Internal") + +if free_slip_upper: + stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) * Gamma, "Upper") +else: + stokes.add_essential_bc((0.0,0.0), "Upper") + +if free_slip_lower: + stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) * Gamma, "Lower") +else: + stokes.add_essential_bc((0.0,0.0), "Lower") + +stokes.solve() + + + +# + +# Null space evaluation + +I0 = uw.maths.Integral(meshball, v_theta_fn_xy.dot(v_soln.sym)) +norm = I0.evaluate() +I0.fn = v_theta_fn_xy.dot(v_theta_fn_xy) +vnorm = I0.evaluate() + +print(norm/vnorm, vnorm) + +with meshball.access(v_soln): + dv = uw.function.evaluate(norm * v_theta_fn_xy, v_soln.coords) / vnorm + v_soln.data[...] -= dv + +with meshball.access(v_soln1): + v_soln1.data[...] = v_soln.data[...] + +# - + +pressure_solver = uw.systems.Projection(meshball, p_cont) +pressure_solver.uw_function = p_soln.sym[0] +pressure_solver.smoothing = 1.0e-3 + +# + +## Now solve with normals from nodal projection + +stokes._reset() + +stokes.bodyforce = sympy.Matrix([0,0]) +Gamma = meshball.Gamma / sympy.sqrt(meshball.Gamma.dot(meshball.Gamma)) +Gamma = norm_v.sym + +stokes.add_natural_bc(-t_init * unit_rvec, "Internal") + +if free_slip_upper: + stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) * Gamma, "Upper") +else: + stokes.add_essential_bc((0.0,0.0), "Upper") + +if free_slip_lower: + stokes.add_natural_bc(10000 * Gamma.dot(v_soln.sym) * Gamma, "Lower") +else: + stokes.add_essential_bc((0.0,0.0), "Lower") + +stokes.solve(zero_init_guess=False) + +# + +# Null space evaluation + +I0 = uw.maths.Integral(meshball, v_theta_fn_xy.dot(v_soln.sym)) +norm = I0.evaluate() + +with meshball.access(v_soln): + dv = uw.function.evaluate(norm * v_theta_fn_xy, v_soln.coords) / vnorm + v_soln.data[...] -= dv + +print(norm/vnorm, vnorm) +# -9.662093930530614e-09 0.024291704747453444 + +norm = I0.evaluate() +# - + +I0 = uw.maths.Integral(meshball, v_theta_fn_xy.dot(v_soln.sym)) +norm = I0.evaluate() +print(norm/vnorm) + +# Pressure at mesh nodes +pressure_solver.solve() + +# + +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshball) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym) + velocity_points.point_data["V0"] = vis.vector_fn_to_pv_points(velocity_points, v_soln1.sym) + velocity_points.point_data["dV"] = velocity_points.point_data["V"] - velocity_points.point_data["V0"] + + pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_cont.sym) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_init) + pvmesh.point_data["V0"] = vis.vector_fn_to_pv_points(pvmesh, v_soln1.sym) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) + pvmesh.point_data["dV"] = pvmesh.point_data["V"] - pvmesh.point_data["V0"] + pvmesh.point_data["Vmag"] = np.hypot(pvmesh.point_data["V"][:,0],pvmesh.point_data["V"][:,1]) + + skip = 3 + points = np.zeros((meshball._centroids[::skip].shape[0], 3)) + points[:, 0] = meshball._centroids[::skip, 0] + points[:, 1] = meshball._centroids[::skip, 1] + point_cloud = pv.PolyData(points) + + pvstream = pvmesh.streamlines_from_source( + point_cloud, vectors="V", + integration_direction="both", + integrator_type=2, + surface_streamlines=True, + initial_step_length=0.01, + max_time=0.25, + max_steps=500 + ) + + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Grey", + scalars="Vmag", + show_edges=True, + use_transparency=False, + opacity=1.0, + show_scalar_bar=True + ) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=3) + pl.add_arrows(velocity_points.points, velocity_points.point_data["V0"], mag=3, color="Black") + # pl.add_arrows(velocity_points.points, velocity_points.point_data["dV"], mag=100, color="Black") + pl.add_mesh(pvstream, opacity=0.3, show_scalar_bar=False) + + + pl.show(cpos="xy") +# - + +vsol_rms = np.sqrt(velocity_points.point_data["V"][:, 0] ** 2 + velocity_points.point_data["V"][:, 1] ** 2).mean() +vsol_rms diff --git a/main/_sources/Notebooks/Examples-Stokes_Kernels/Ex_Stokes_Cartesian_Kernels.py b/main/_sources/Notebooks/Examples-Stokes_Kernels/Ex_Stokes_Cartesian_Kernels.py new file mode 100644 index 0000000..69f9c4d --- /dev/null +++ b/main/_sources/Notebooks/Examples-Stokes_Kernels/Ex_Stokes_Cartesian_Kernels.py @@ -0,0 +1,288 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Cartesian Stokes Kernels +# +# Mesh with embedded internal surface +# +# This allows us to introduce an internal force integral + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import petsc4py +from petsc4py import PETSc + +import underworld3 as uw +from underworld3.systems import Stokes +from underworld3 import function + +import numpy as np +import sympy + +res = 0.05 +resH = 0.25 + +options = PETSc.Options() + +import os + +os.environ["SYMPY_USE_CACHE"] = "no" + +# + +from enum import Enum + +class boundaries(Enum): + Top = 1 + Bottom = 2 + Internal = 3 + Left = 4 + Right = 5 + + +xmin = 0.0 +xmax = 1.0 +ymin = 0.0 +ymax = 1.0 +yint = 0.66 + +cellSize = res + +if uw.mpi.rank == 0: + import gmsh + + gmsh.initialize() + gmsh.option.setNumber("General.Verbosity", 0) + gmsh.model.add("KernelBox") + + p1 = gmsh.model.geo.add_point(xmin, ymin, 0.0, meshSize=cellSize) + p2 = gmsh.model.geo.add_point(xmax, ymin, 0.0, meshSize=cellSize) + p3 = gmsh.model.geo.add_point(xmin, ymax, 0.0, meshSize=cellSize) + p4 = gmsh.model.geo.add_point(xmax, ymax, 0.0, meshSize=cellSize) + + # Internal surface points + p5 = gmsh.model.geo.add_point(xmin, yint, 0.0, meshSize=cellSize) + p6 = gmsh.model.geo.add_point(xmax, yint, 0.0, meshSize=cellSize) + + l1 = gmsh.model.geo.add_line(p1, p2) + l2 = gmsh.model.geo.add_line(p3, p4) + l3 = gmsh.model.geo.add_line(p1, p5) + l4 = gmsh.model.geo.add_line(p5, p3) + l5 = gmsh.model.geo.add_line(p2, p6) + l6 = gmsh.model.geo.add_line(p6, p4) + l7 = gmsh.model.geo.add_line(p5, p6) + + cl1 = gmsh.model.geo.add_curve_loop((l1, l5, -l7, -l3)) + cl2 = gmsh.model.geo.add_curve_loop((-l2, -l4, l7, l6)) + + gmsh.model.geo.synchronize() + + # gmsh.model.geo.add_curve_loops([cl1,cl2]) + surface1 = gmsh.model.geo.add_plane_surface([cl1]) + surface2 = gmsh.model.geo.add_plane_surface([cl2]) + + gmsh.model.geo.synchronize() + + # Add Physical groups for boundaries + gmsh.model.add_physical_group(1, [l1,], boundaries.Bottom.value) + gmsh.model.set_physical_name(1, l1, boundaries.Bottom.name) + gmsh.model.add_physical_group(1, [l2], boundaries.Top.value) + gmsh.model.set_physical_name(1, l2, boundaries.Top.name) + gmsh.model.add_physical_group(1, [l3, l4], boundaries.Left.value) + gmsh.model.set_physical_name(1, l3, boundaries.Left.name) + gmsh.model.add_physical_group(1, [l5,l6], boundaries.Right.value) + gmsh.model.set_physical_name(1, l4, boundaries.Right.name) + + gmsh.model.add_physical_group(1, [l7], boundaries.Internal.value) + gmsh.model.set_physical_name(1, l7, boundaries.Internal.name) + + gmsh.model.addPhysicalGroup(2, [surface1,surface2], 99999) + gmsh.model.setPhysicalName(2, 99999, "Elements") + + gmsh.model.geo.synchronize() + + gmsh.model.mesh.generate(2) + gmsh.write("tmp_cart_kernel_mesh.msh") + + # gmsh.fltk.run() + + gmsh.finalize() + +kernel_mesh = uw.discretisation.Mesh( + "tmp_cart_kernel_mesh.msh", + degree=1, + qdegree=3, + useMultipleTags=True, + useRegions=False, + markVertices=True, + boundaries=boundaries, + coordinate_system_type=None, + refinement=0, + refinement_callback=None, + return_coords_to_bounds=None, + ) + +x,y = kernel_mesh.X + +## The internal bc is not read correctly +## We set useRegions=False and then unstack the boundaries +## OK because we know (because it's our mesh) that the face-sets capture the boundaries + +uw.adaptivity._dm_unstack_bcs(kernel_mesh.dm, kernel_mesh.boundaries, "Face Sets") + +kernel_mesh.dm.view() + +# - +v_soln = uw.discretisation.MeshVariable(r"\mathbf{u}", kernel_mesh, 2, degree=2) +p_soln = uw.discretisation.MeshVariable(r"p", kernel_mesh, 1, degree=1, continuous=False) +p_cont = uw.discretisation.MeshVariable(r"pc", kernel_mesh, 1, degree=1, continuous=True) +syy = uw.discretisation.MeshVariable(r"Syy", kernel_mesh, 1, degree=1, continuous=True) + +if 0 and uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(kernel_mesh) + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh(pvmesh, "Grey", "wireframe") + + pl.add_mesh( + pvmesh, + cmap="Greens", + edge_color="Grey", + show_edges=True, + use_transparency=False, + clim=[0.66, 1], + opacity=0.75, + ) + + pl.show(cpos="xy") + +# + +# Create Stokes object + +stokes = Stokes( + kernel_mesh, velocityField=v_soln, pressureField=p_soln, solver_name="stokes" +) + +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.viscosity = 1.0 +stokes.saddle_preconditioner = 1.0 + +t_init = sympy.cos(3*x*sympy.pi) * sympy.exp(-1000.0 * ((y - yint) ** 2)) + +stokes.add_essential_bc(sympy.Matrix([sympy.oo, 0.0]), "Top") +stokes.add_essential_bc(sympy.Matrix([sympy.oo, 0.0]), "Bottom") +stokes.add_essential_bc(sympy.Matrix([0.0,sympy.oo]), "Left") +stokes.add_essential_bc(sympy.Matrix([0.0,sympy.oo]), "Right") + +stokes.add_natural_bc(sympy.Matrix([0.0, -t_init]), "Internal") + +stokes.bodyforce = sympy.Matrix([0,0]) + + +# + +pressure_solver = uw.systems.Projection(kernel_mesh, p_cont) +pressure_solver.uw_function = p_soln.sym[0] +pressure_solver.smoothing = 1.0e-3 + +stress_solver = uw.systems.Projection(kernel_mesh, syy) +stress_solver.uw_function = stokes.constitutive_model.flux[1,1] +stress_solver.smoothing = 0.0e-6 + + +# - + +stokes.petsc_options.setValue("ksp_monitor", None) +stokes.petsc_options.setValue("snes_monitor", None) +stokes.petsc_options.delValue("snes_converged_reason") +stokes.solve() + +# Pressure at mesh nodes +pressure_solver.solve() +stress_solver.solve() + +# + +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(kernel_mesh) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym * sympy.exp(-100000.0 * ((y - 1) ** 2) )) + velocity_points.point_data["V"][:,0] = 0 + velocity_points.point_data["Syy"] = vis.scalar_fn_to_pv_points(velocity_points, syy.sym) + velocity_points.point_data["SyyV"] = velocity_points.point_data["V"].copy() * 0.0 + velocity_points.point_data["SyyV"][:,1] = vis.scalar_fn_to_pv_points(velocity_points, syy.sym[0] * sympy.exp(-100000.0 * ((y - 1) ** 2))) + + + pvmesh.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh, p_cont.sym) + pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_init) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym) + + points = np.zeros((kernel_mesh._centroids.shape[0], 3)) + points[:, 0] = kernel_mesh._centroids[:, 0] + points[:, 1] = kernel_mesh._centroids[:, 1] + point_cloud = pv.PolyData(points) + + pvstream = pvmesh.streamlines_from_source( + point_cloud, vectors="V", + integration_direction="both", + integrator_type=2, + surface_streamlines=True, + initial_step_length=0.01, + max_time=1.0, + max_steps=500 + ) + + pl = pv.Plotter(title="Stokes Greens Functions", window_size=(750, 750)) + + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Grey", + scalars="P", + show_edges=True, + use_transparency=False, + opacity=1.0, + show_scalar_bar=False, + ) + + pl.add_mesh(pvstream, opacity=0.3, show_scalar_bar=False) + pl.add_arrows(velocity_points.points, velocity_points.point_data["SyyV"], mag=-1, show_scalar_bar=False) + + + pl.show(cpos="xy") +# - +stokes.view() + + +stokes.constitutive_model.view() + +stokes.constitutive_model.C + +stokes.constitutive_model.c + + + + diff --git a/main/_sources/Notebooks/Examples-SwarmAndParticles/CheckSwarmLayout.py b/main/_sources/Notebooks/Examples-SwarmAndParticles/CheckSwarmLayout.py new file mode 100644 index 0000000..9a53fc2 --- /dev/null +++ b/main/_sources/Notebooks/Examples-SwarmAndParticles/CheckSwarmLayout.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python +# coding: utf-8 +# %% +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# %% +import petsc4py +from petsc4py import PETSc +import underworld3 as uw +from underworld3.swarm import SwarmPICLayout +import numpy as np + + +# %% +n_els = 2 +mesh = uw.meshing.UnstructuredSimplexBox( + minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0), + cellSize=1 / n_els, qdegree=2, refinement=0 +) +mesh.dm.view() + + +# %% +v = uw.discretisation.MeshVariable("U", mesh, mesh.dim, degree=2) +p = uw.discretisation.MeshVariable("P", mesh, 1, degree=1) + + +# %% +swarm = uw.swarm.Swarm(mesh=mesh) +swarm.populate_petsc( + fill_param=2, + layout = SwarmPICLayout.GAUSS +) + + +# %% +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh) + + + # with mesh.access(): + # usol = v_soln.data.copy() + + # with mesh.access(): + # pvmesh.point_data["Vmag"] = uw.function.evalf( + # sympy.sqrt(v_soln.sym.dot(v_soln.sym)), mesh.data + # ) + # pvmesh.point_data["P"] = uw.function.evalf(p_soln.fn, mesh.data) + + + # v_vectors = np.zeros((mesh.data.shape[0], 3)) + # v_vectors[:, 0] = uw.function.evalf(v_soln[0].sym, mesh.data) + # v_vectors[:, 1] = uw.function.evalf(v_soln[1].sym, mesh.data) + # pvmesh.point_data["V"] = v_vectors + + # arrow_loc = np.zeros((v_soln.coords.shape[0], 3)) + # arrow_loc[:, 0:2] = v_soln.coords[...] + + # arrow_length = np.zeros((v_soln.coords.shape[0], 3)) + # arrow_length[:, 0:2] = usol[...] + + # point sources at cell centres + points = np.zeros((mesh._centroids.shape[0], 3)) + points[:, 0] = mesh._centroids[:, 0] + points[:, 1] = mesh._centroids[:, 1] + point_cloud = pv.PolyData(points) + + spoints = vis.swarm_to_pv_cloud(swarm) + spoint_cloud = pv.PolyData(spoints) + + pl = pv.Plotter(window_size=(1000, 750)) + + # pl.add_arrows(arrow_loc, arrow_length, mag=0.025 / U0, opacity=0.75) + + pl.add_points(spoint_cloud,color="Black", + render_points_as_spheres=False, + point_size=5, opacity=0.66 + ) + + pl.add_mesh(pvmesh,'Black', 'wireframe', opacity=0.75) + # pl.add_mesh(pvstream) + + # pl.remove_scalar_bar("mag") + + pl.show() + + +# %% +with swarm.access(): + print(swarm.data.shape) + +# %% +14*6 + +# %% +mesh._centroids.shape + +# %% diff --git a/main/_sources/Notebooks/Examples-UW-2to3/Ex_stokes_Sinker-UWComp-Linear.py b/main/_sources/Notebooks/Examples-UW-2to3/Ex_stokes_Sinker-UWComp-Linear.py new file mode 100644 index 0000000..dff78b0 --- /dev/null +++ b/main/_sources/Notebooks/Examples-UW-2to3/Ex_stokes_Sinker-UWComp-Linear.py @@ -0,0 +1,549 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# + [markdown] magic_args="[markdown]" +# ''Stokes Sinker'' +# ====== +# +# Testing the Stokes sinker problem between UW2 and UW3. This system consists of a dense, high viscosity sphere falling through a background lower density and a viscous fluid. +# +# +# +# +# - + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# %% +import underworld as uw2 +from underworld import function as fn +import underworld.visualisation as vis +import numpy as np +import math + +rank = uw2.mpi.rank + +# + [markdown] magic_args="[markdown]" +# Setup parameters +# ----- +# +# Set simulation parameters for the test and position of the spherical sinker. +# - + +# %% +# Set the resolution. +res = 32 + +# Set size and position of dense sphere. +sphereRadius = 0.1 +sphereCentre = (0.0, 0.7) + +# define some names for our index +materialLightIndex = 0 +materialHeavyIndex = 1 + + +# Set constants for the viscosity and density of the sinker. +viscBG = 1.0 +viscSphere = 10.0 + +densityBG = 1.0 +densitySphere = 10.0 + + +# location of tracer at bottom of sinker +x_pos = sphereCentre[0] +y_pos = sphereCentre[1] - sphereRadius + + +nsteps = 10 + + +swarmGPC = 4 + + +# + [markdown] magic_args="[markdown]" +# Create UW2 version +# ------ +# - + +# %% +def uw2_stokesSinker(): + + mesh = uw2.mesh.FeMesh_Cartesian( + elementType=("Q1/dQ0"), + elementRes=(int(res * 2), int(res)), + minCoord=(-1.0, 0.0), + maxCoord=(1.0, 1.0), + ) + + velocityField = mesh.add_variable(nodeDofCount=2) + pressureField = mesh.subMesh.add_variable(nodeDofCount=1) + + velocityField.data[:] = [0.0, 0.0] + pressureField.data[:] = 0.0 + + # Create the swarm and an advector associated with it + swarm = uw2.swarm.Swarm(mesh=mesh) + advector = uw2.systems.SwarmAdvector( + swarm=swarm, velocityField=velocityField, order=2 + ) + + # Add a data variable which will store an index to determine material. + materialIndex = swarm.add_variable(dataType="int", count=1) + + # Create a layout object that will populate the swarm across the whole domain. + swarmLayout = uw2.swarm.layouts.PerCellGaussLayout( + swarm=swarm, gaussPointCount=swarmGPC + ) + # swarmLayout = uw2.swarm.layouts.PerCellSpaceFillerLayout( swarm=swarm, particlesPerCell=swarmFill ) + + # Go ahead and populate the swarm. + swarm.populate_using_layout(layout=swarmLayout) + + # create a function for a sphere. returns `True` if query is inside sphere, `False` otherwise. + coord = fn.input() - sphereCentre + fn_sphere = fn.math.dot(coord, coord) < sphereRadius**2 + + # set up the condition for being in a sphere. If not in sphere then will return light index. + conditions = [(fn_sphere, materialHeavyIndex), (True, materialLightIndex)] + + # Execute the branching conditional function, evaluating at the location of each particle in the swarm. + # The results are copied into the materialIndex swarm variable. + materialIndex.data[:] = fn.branching.conditional(conditions).evaluate(swarm) + + # build a tracer swarm with one particle + tracerSwarm = uw2.swarm.Swarm(mesh) + advector_tracer = uw2.systems.SwarmAdvector( + swarm=tracerSwarm, velocityField=velocityField, order=2 + ) + + # build a numpy array with one particle, specifying it's exact location + coord_array = np.array(object=(x_pos, y_pos), ndmin=2) + tracerSwarm.add_particles_with_coordinates(coord_array) + + tracer = numpy.zeros(shape=(1, 2)) + tracer[:, 0], tracer[:, 1] = x_pos, y_pos + + fig1 = vis.Figure(figsize=(800, 400)) + fig1.Points(swarm, materialIndex, colourBar=False, pointSize=2.0) + fig1.VectorArrows(mesh, velocityField) + fig1.show() + + # Here we set a viscosity value of '1.' for both materials + mappingDictViscosity = {materialLightIndex: viscBG, materialHeavyIndex: viscSphere} + # Create the viscosity map function. + viscosityMapFn = fn.branching.map( + fn_key=materialIndex, mapping=mappingDictViscosity + ) + # Here we set a density of '0.' for the lightMaterial, and '1.' for the heavymaterial. + mappingDictDensity = { + materialLightIndex: densityBG, + materialHeavyIndex: densitySphere, + } + # Create the density map function. + densityFn = fn.branching.map(fn_key=materialIndex, mapping=mappingDictDensity) + + # And the final buoyancy force function. + z_hat = (0.0, 1.0) + buoyancyFn = -densityFn * z_hat + + iWalls = mesh.specialSets["MinI_VertexSet"] + mesh.specialSets["MaxI_VertexSet"] + jWalls = mesh.specialSets["MinJ_VertexSet"] + mesh.specialSets["MaxJ_VertexSet"] + + freeslipBC = uw2.conditions.DirichletCondition( + variable=velocityField, indexSetsPerDof=(iWalls, jWalls) + ) + + noslipBC = uw2.conditions.DirichletCondition( + variable=velocityField, indexSetsPerDof=(iWalls + jWalls, iWalls + jWalls) + ) + + stokes = uw2.systems.Stokes( + velocityField=velocityField, + pressureField=pressureField, + voronoi_swarm=swarm, + conditions=noslipBC, + fn_viscosity=viscosityMapFn, + fn_bodyforce=buoyancyFn, + ) + + solver = uw2.systems.Solver(stokes) + + top = mesh.specialSets["MaxJ_VertexSet"] + surfaceArea = uw2.utils.Integral( + fn=1.0, mesh=mesh, integrationType="surface", surfaceIndexSet=top + ) + surfacePressureIntegral = uw2.utils.Integral( + fn=pressureField, mesh=mesh, integrationType="surface", surfaceIndexSet=top + ) + + # a callback function to calibrate the pressure - will pass to solver later + def pressure_calibrate(): + (area,) = surfaceArea.evaluate() + (p0,) = surfacePressureIntegral.evaluate() + offset = p0 / area + if rank == 0: + print( + "Zeroing pressure using mean upper surface pressure {}".format(offset) + ) + pressureField.data[:] -= offset + + vdotv = fn.math.dot(velocityField, velocityField) + + # Stepping. Initialise time and timestep. + time = 0.0 + step = 0 + + tSinker = np.zeros(nsteps) + ySinker0 = np.zeros(nsteps) + + ySinker1 = np.zeros(nsteps) + + # Perform 10 steps + while step < nsteps: + # Get velocity solution - using callback + solver.solve() + # Calculate the RMS velocity + vrms = math.sqrt(mesh.integrate(vdotv)[0] / mesh.integrate(1.0)[0]) + + if rank == 0: + ymin0 = np.copy(tracerSwarm.data[:, 1].min()) + ymin1 = np.copy(tracer[:, 1].min()) + + ySinker0[step] = ymin0 + ySinker1[step] = ymin1 + + tSinker[step] = time + + print( + "step = {0:6d}; time = {1:.3e}; v_rms = {2:.3e}; height0 = {3:.3e}; height1 = {3:.3e}".format( + step, time, vrms, ymin0, ymin1 + ) + ) + + # Retrieve the maximum possible timestep for the advection system. + dt = advector.get_max_dt() + # Advect using this timestep size. + advector.integrate(dt) + advector_tracer.integrate(dt) + + vel_on_tracer = velocityField.evaluate(tracer) + tracer += dt * vel_on_tracer + + step += 1 + time += dt + + if rank == 0: + print( + "Initial position: t = {0:.3f}, y = {1:.3f}".format(tSinker[0], ySinker0[0]) + ) + print( + "Final position: t = {0:.3f}, y = {1:.3f}".format( + tSinker[nsteps - 1], ySinker0[nsteps - 1] + ) + ) + + uw2.utils.matplotlib_inline() + import matplotlib.pyplot as pyplot + + fig = pyplot.figure() + fig.set_size_inches(12, 6) + ax = fig.add_subplot(1, 1, 1) + ax.plot(tSinker, ySinker0) + ax.plot(tSinker, ySinker1) + ax.set_xlabel("Time") + ax.set_ylabel("Sinker position") + + fig1.show() + + return tSinker, ySinker0, ySinker1 + + +# + [markdown] magic_args="[markdown]" +# Create UW3 version +# ------ +# - + +# %% +from petsc4py import PETSc +import underworld3 as uw3 +from underworld3.systems import Stokes +import numpy +import sympy +from mpi4py import MPI + +options = PETSc.Options() + +# %% +def uw3_stokesSinker(render=True): + + sys = PETSc.Sys() + sys.pushErrorHandler("traceback") + + options = PETSc.Options() + options["snes_converged_reason"] = None + options["snes_monitor_short"] = None + + mesh = uw3.meshing.StructuredQuadBox( + elementRes=(int(res), int(res)), minCoords=(-1.0, 0.0), maxCoords=(1.0, 1.0) + ) + + v = uw3.discretisation.MeshVariable("U", mesh, mesh.dim, degree=2) + p = uw3.discretisation.MeshVariable("P", mesh, 1, degree=1) + + stokes = uw3.systems.Stokes(mesh, velocityField=v, pressureField=p) + + ### free slip. + ### note with petsc we always need to provide a vector of correct cardinality. + # stokes.add_dirichlet_bc( (0.,0.), ["Bottom", "Top"], 1 ) # top/bottom: components, function, markers + # stokes.add_dirichlet_bc( (0.,0.), ["Left", "Right"], 0 ) # left/right: components, function, markers + + ### No slip (?) + sol_vel = sympy.Matrix([0, 0]) + + stokes.add_dirichlet_bc( + sol_vel, ["Top", "Bottom"], [0, 1] + ) # top/bottom: components, function, markers + stokes.add_dirichlet_bc( + sol_vel, ["Left", "Right"], [0, 1] + ) # left/right: components, function, markers + + swarm = uw3.swarm.Swarm(mesh=mesh) + material = uw3.swarm.IndexSwarmVariable("M", swarm, indices=4) + swarm.populate(fill_param=swarmGPC) + + blob = numpy.array( + # [[ 0.25, 0.75, 0.1, 1], + # [ 0.45, 0.70, 0.05, 2], + # [ 0.65, 0.60, 0.06, 3], + [[sphereCentre[0], sphereCentre[1], sphereRadius, 1]] + ) + # [ 0.65, 0.20, 0.06, 2], + # [ 0.45, 0.20, 0.12, 3] ]) + + with swarm.access(material): + material.data[...] = materialLightIndex + + for i in range(blob.shape[0]): + cx, cy, r, m = blob[i, :] + inside = (swarm.data[:, 0] - cx) ** 2 + ( + swarm.data[:, 1] - cy + ) ** 2 < r**2 + material.data[inside] = materialHeavyIndex + + tracer = numpy.zeros(shape=(1, 2)) + tracer[:, 0], tracer[:, 1] = x_pos, y_pos + + mat_density = numpy.array([densityBG, densitySphere]) + + density = mat_density[0] * material.sym[0] + mat_density[1] * material.sym[1] + + mat_viscosity = numpy.array([viscBG, viscSphere]) + + viscosity = mat_viscosity[0] * material.sym[0] + mat_viscosity[1] * material.sym[1] + + def plot_fig(): + + import numpy as np + import pyvista as pv + import vtk + + pv.global_theme.background = "white" + pv.global_theme.window_size = [750, 750] + pv.global_theme.antialiasing = True + pv.global_theme.jupyter_backend = "trame" + pv.global_theme.smooth_shading = True + + mesh.vtk("tempMsh.vtk") + pvmesh = pv.read("tempMsh.vtk") + + with swarm.access(): + points = numpy.zeros((swarm.data.shape[0], 3)) + points[:, 0] = swarm.data[:, 0] + points[:, 1] = swarm.data[:, 1] + points[:, 2] = 0.0 + + point_cloud = pv.PolyData(points) + + with swarm.access(): + point_cloud.point_data["M"] = material.data.copy() + + pl = pv.Plotter(notebook=True) + + pl.add_mesh(pvmesh, "Black", "wireframe") + + # pl.add_points(point_cloud, color="Black", + # render_points_as_spheres=False, + # point_size=2.5, opacity=0.75) + + pl.add_mesh( + point_cloud, + cmap="coolwarm", + edge_color="Black", + show_edges=False, + scalars="M", + use_transparency=False, + opacity=0.95, + ) + + pl.show(cpos="xy") + + if render: + plot_fig() + + stokes.constitutive_model = uw3.constitutive_models.ViscousFlowModel + stokes.constitutive_model.material_properties = ( + stokes.constitutive_model.Parameters(viscosity=viscosity) + ) + + # stokes.viscosity = viscosity + + stokes.bodyforce = -1.0 * density * mesh.N.j + + step = 0 + time = 0.0 + + tSinker = np.zeros(nsteps) + ySinker = np.zeros(nsteps) + + while step < nsteps: + + ### solve stokes + stokes.solve() + + if MPI.COMM_WORLD.rank == 0: + ymin = tracer[:, 1].min() + ySinker[step] = ymin + tSinker[step] = time + print( + f"Step: {str(step).rjust(3)}, time: {time:6.2f}, tracer: {ymin:6.2f}" + ) # , vrms {vrms_val:.3e}") + + ### estimate dt + dt = stokes.estimate_dt() + + with swarm.access(): + vel_on_particles = uw3.function.evaluate( + stokes.u.fn, swarm.particle_coordinates.data + ) + + ### advect swarm + with swarm.access(swarm.particle_coordinates): + swarm.particle_coordinates.data[:] += dt * vel_on_particles + + vel_on_tracer = uw3.function.evaluate(stokes.u.fn, tracer) + tracer += dt * vel_on_tracer + + # if MPI.COMM_WORLD.rank==0: + # print('step = {0:6d}; time = {1:.3e}; v_rms = {2:.3e}; height = {3:.3e}' + # .format(step,time,vrms,ymin)) + + step += 1 + time += dt + + if rank == 0: + print( + "Initial position: t = {0:.3f}, y = {1:.3f}".format(tSinker[0], ySinker[0]) + ) + print( + "Final position: t = {0:.3f}, y = {1:.3f}".format( + tSinker[nsteps - 1], ySinker[nsteps - 1] + ) + ) + + uw2.utils.matplotlib_inline() + import matplotlib.pyplot as pyplot + + fig = pyplot.figure() + fig.set_size_inches(12, 6) + ax = fig.add_subplot(1, 1, 1) + ax.plot(tSinker, ySinker) + ax.set_xlabel("Time") + ax.set_ylabel("Sinker position") + + # fig1.show() + + if render: + plot_fig() + + return tSinker, ySinker + + +# %% +tSinker_UW2, ySinker0_UW2, ySinker1_UW2 = uw2_stokesSinker() + +# %% +tSinker_UW3, ySinker_UW3 = uw3_stokesSinker() + +# + [markdown] magic_args="[markdown]" +# Compare velocity of tracer in both models to terminal velocity estimation +# ------ +# - + +# ### Terminal velocity estimation is closest to model with no slip boundaries, free slip results in a higher velocity + + +# ## UW2 and UW3 velocities match when the viscosities in UW3 are doubled or densities are halved + + +stokes_vel0 = (-1 * (2 * sphereRadius) ** 2 * (densitySphere - densityBG)) / ( + 18 * viscBG +) +stokes_vel1 = -1 * (2 / 9) * ((densitySphere - densityBG) / viscBG) * sphereRadius**2 + +# print(f'stokes vel0: {stokes_vel0}, stokes vel1: {stokes_vel1}') + +time = np.arange(0, 7.5, 0.1) +terminalStokes = stokes_vel0 * time + +UW2_vel = (ySinker0_UW2[0] - ySinker0_UW2[-1]) / (tSinker_UW2[0] - tSinker_UW2[-1]) +UW3_vel = (ySinker_UW3[0] - ySinker_UW3[-1]) / (tSinker_UW3[0] - tSinker_UW3[-1]) + + +# %% +print( + f"\n\n\n alaytical velocity: {stokes_vel0}, UW2 velocity: {UW2_vel}, UW3 velocity: {UW3_vel/2.} \n\n\n" +) + +# %% +if rank == 0: + import matplotlib.pyplot as pyplot + + fig = pyplot.figure() + fig.set_size_inches(12, 6) + ax = fig.add_subplot(1, 1, 1) + ax.plot(tSinker_UW2, ySinker0_UW2, c="blue", ls="--", label="UW2") + + ax.plot(tSinker_UW2, ySinker1_UW2, c="orange", ls="-.", label="UW2") + + ax.plot(tSinker_UW3, ySinker_UW3, c="red", ls=":", label="UW3") + + ax.plot( + time, + (sphereCentre[1] - sphereRadius) + terminalStokes, + c="k", + ls=":", + label="Terminal velocity", + ) + + ax.legend() + + ax.set_xlabel("Time") + ax.set_ylabel("Sinker position") + + # fig1.show() + diff --git a/main/_sources/Notebooks/Examples-UW-2to3/Ex_stokes_Sinker-UWComp-NonLinear.py b/main/_sources/Notebooks/Examples-UW-2to3/Ex_stokes_Sinker-UWComp-NonLinear.py new file mode 100644 index 0000000..a292850 --- /dev/null +++ b/main/_sources/Notebooks/Examples-UW-2to3/Ex_stokes_Sinker-UWComp-NonLinear.py @@ -0,0 +1,571 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# + [markdown] magic_args="[markdown]" +# ''Non-linear Stokes Sinker'' +# ====== +# +# Testing a non-linear implementation of the Stokes sinker between UW2 and UW3. This system consists of a dense, high viscosity sphere falling through a background lower density and a non-linear viscoplastic fluid (strain-rate dependent). +# +# +# +# - + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# %% +import underworld as uw2 +from underworld import function as fn +import underworld.visualisation as vis +import numpy as np +import math + +# + [markdown] magic_args="[markdown]" +# Setup parameters +# ----- +# +# Set simulation parameters for the test and position of the spherical sinker. +# - + +# %% +# Set the resolution. +res = 42 + +# Set size and position of dense sphere. +sphereRadius = 0.1 +sphereCentre = (0.0, 0.7) + +# define some names for our index +materialLightIndex = 0 +materialHeavyIndex = 1 + +eta_min = 1e-2 +eta_max = 1e2 + +# Set constants for the viscosity and density of the sinker. +viscBG = eta_max +viscSphere = eta_max + +densityBG = 1.0 +densitySphere = 10.0 + +Cohesion = 0.15 + + +# location of tracer at bottom of sinker +x_pos = sphereCentre[0] +y_pos = sphereCentre[1] - sphereRadius + + +nsteps = 10 + + +swarmGPC = 2 + + +# + [markdown] magic_args="[markdown]" +# Create mesh and finite element variables +# ------ +# - + +# %% +def uw2_stokesSinker(): + + mesh = uw2.mesh.FeMesh_Cartesian( + elementType=("Q1/dQ0"), + elementRes=(int(res), int(res)), + minCoord=(-1.0, 0.0), + maxCoord=(1.0, 1.0), + ) + + velocityField = mesh.add_variable(nodeDofCount=2) + pressureField = mesh.subMesh.add_variable(nodeDofCount=1) + + velocityField.data[:] = [0.0, 0.0] + pressureField.data[:] = 0.0 + + # Create the swarm and an advector associated with it + swarm = uw2.swarm.Swarm(mesh=mesh) + advector = uw2.systems.SwarmAdvector( + swarm=swarm, velocityField=velocityField, order=2 + ) + + # Add a data variable which will store an index to determine material. + materialIndex = swarm.add_variable(dataType="int", count=1) + + # Create a layout object that will populate the swarm across the whole domain. + swarmLayout = uw2.swarm.layouts.PerCellGaussLayout( + swarm=swarm, gaussPointCount=swarmGPC + ) + # swarmLayout = uw2.swarm.layouts.PerCellSpaceFillerLayout( swarm=swarm, particlesPerCell=swarmFill ) + + # Go ahead and populate the swarm. + swarm.populate_using_layout(layout=swarmLayout) + + # create a function for a sphere. returns `True` if query is inside sphere, `False` otherwise. + coord = fn.input() - sphereCentre + fn_sphere = fn.math.dot(coord, coord) < sphereRadius**2 + + # set up the condition for being in a sphere. If not in sphere then will return light index. + conditions = [(fn_sphere, materialHeavyIndex), (True, materialLightIndex)] + + # Execute the branching conditional function, evaluating at the location of each particle in the swarm. + # The results are copied into the materialIndex swarm variable. + materialIndex.data[:] = fn.branching.conditional(conditions).evaluate(swarm) + + # build a tracer swarm with one particle + tracerSwarm = uw2.swarm.Swarm(mesh) + advector_tracer = uw2.systems.SwarmAdvector( + swarm=tracerSwarm, velocityField=velocityField, order=2 + ) + + # build a numpy array with one particle, specifying it's exact location + coord_array = np.array(object=(x_pos, y_pos), ndmin=2) + tracerSwarm.add_particles_with_coordinates(coord_array) + + tracer = numpy.zeros(shape=(1, 2)) + tracer[:, 0], tracer[:, 1] = x_pos, y_pos + + fig1 = vis.Figure(figsize=(800, 400)) + fig1.Points(swarm, materialIndex, colourBar=False, pointSize=2.0) + fig1.VectorArrows(mesh, velocityField) + fig1.show() + + # The yeilding of the BG material is dependent on the SR. + strainRate_2ndInvariant = fn.tensor.second_invariant( + fn.tensor.symmetric(velocityField.fn_gradient) + ) + + # vonMises = (Cohesion_BG / (2.*((strainRate_2ndInvariant+1.0e-20)*ref_SR))) / ref_viscosity + + vonMises = Cohesion / (2.0 * ((strainRate_2ndInvariant + 1.0e-20))) + + viscoplasticBG = fn.exception.SafeMaths( + fn.misc.min(fn.misc.max(fn.misc.min(vonMises, viscBG), eta_min), eta_max) + ) + + # Here we set a viscosity value of '1.' for both materials + mappingDictViscosity = { + materialLightIndex: viscoplasticBG, + materialHeavyIndex: viscSphere, + } + # Create the viscosity map function. + viscosityMapFn = fn.branching.map( + fn_key=materialIndex, mapping=mappingDictViscosity + ) + # Here we set a density of '0.' for the lightMaterial, and '1.' for the heavymaterial. + mappingDictDensity = { + materialLightIndex: densityBG, + materialHeavyIndex: densitySphere, + } + # Create the density map function. + densityFn = fn.branching.map(fn_key=materialIndex, mapping=mappingDictDensity) + + # And the final buoyancy force function. + z_hat = (0.0, 1.0) + buoyancyFn = -densityFn * z_hat + + iWalls = mesh.specialSets["MinI_VertexSet"] + mesh.specialSets["MaxI_VertexSet"] + jWalls = mesh.specialSets["MinJ_VertexSet"] + mesh.specialSets["MaxJ_VertexSet"] + + freeslipBC = uw2.conditions.DirichletCondition( + variable=velocityField, indexSetsPerDof=(iWalls, jWalls) + ) + + noslipBC = uw2.conditions.DirichletCondition( + variable=velocityField, indexSetsPerDof=(iWalls + jWalls, iWalls + jWalls) + ) + + stokes = uw2.systems.Stokes( + velocityField=velocityField, + pressureField=pressureField, + voronoi_swarm=swarm, + conditions=noslipBC, + fn_viscosity=viscosityMapFn, + fn_bodyforce=buoyancyFn, + ) + + solver = uw2.systems.Solver(stokes) + # solver.set_inner_method("lu") + # solver.set_inner_rtol(inner_rtol) + # solver.set_outer_rtol(10*inner_rtol) + # Optional solver settings + + top = mesh.specialSets["MaxJ_VertexSet"] + surfaceArea = uw2.utils.Integral( + fn=1.0, mesh=mesh, integrationType="surface", surfaceIndexSet=top + ) + surfacePressureIntegral = uw2.utils.Integral( + fn=pressureField, mesh=mesh, integrationType="surface", surfaceIndexSet=top + ) + + # a callback function to calibrate the pressure - will pass to solver later + def pressure_calibrate(): + (area,) = surfaceArea.evaluate() + (p0,) = surfacePressureIntegral.evaluate() + offset = p0 / area + if rank == 0: + print( + "Zeroing pressure using mean upper surface pressure {}".format(offset) + ) + pressureField.data[:] -= offset + + vdotv = fn.math.dot(velocityField, velocityField) + + # Stepping. Initialise time and timestep. + time = 0.0 + step = 0 + + tSinker = np.zeros(nsteps) + ySinker0 = np.zeros(nsteps) + + ySinker1 = np.zeros(nsteps) + + # Perform 10 steps + while step < nsteps: + # Get velocity solution - using callback + solver.solve(nonLinearIterate=True) + # Calculate the RMS velocity + vrms = math.sqrt(mesh.integrate(vdotv)[0] / mesh.integrate(1.0)[0]) + + if uw2.mpi.rank == 0: + ymin0 = np.copy(tracerSwarm.data[:, 1].min()) + ymin1 = np.copy(tracer[:, 1].min()) + + ySinker0[step] = ymin0 + ySinker1[step] = ymin1 + + tSinker[step] = time + + print( + "step = {0:6d}; time = {1:.3e}; v_rms = {2:.3e}; height0 = {3:.3e}; height1 = {3:.3e}".format( + step, time, vrms, ymin0, ymin1 + ) + ) + + # Retrieve the maximum possible timestep for the advection system. + dt = advector.get_max_dt() + # Advect using this timestep size. + advector.integrate(dt) + advector_tracer.integrate(dt) + + vel_on_tracer = velocityField.evaluate(tracer) + tracer += dt * vel_on_tracer + + step += 1 + time += dt + + if uw2.mpi.rank == 0: + print( + "Initial position: t = {0:.3f}, y = {1:.3f}".format(tSinker[0], ySinker0[0]) + ) + print( + "Final position: t = {0:.3f}, y = {1:.3f}".format( + tSinker[nsteps - 1], ySinker0[nsteps - 1] + ) + ) + + uw2.utils.matplotlib_inline() + import matplotlib.pyplot as pyplot + + fig = pyplot.figure() + fig.set_size_inches(12, 6) + ax = fig.add_subplot(1, 1, 1) + ax.plot(tSinker, ySinker0) + ax.plot(tSinker, ySinker1) + ax.set_xlabel("Time") + ax.set_ylabel("Sinker position") + + fig1.show() + + return tSinker, ySinker0, ySinker1 + + +# %% +from petsc4py import PETSc +import underworld3 as uw3 +from underworld3.systems import Stokes +import numpy +import sympy +from mpi4py import MPI + +options = PETSc.Options() + +# %% +def uw3_stokesSinker(render=True): + + sys = PETSc.Sys() + sys.pushErrorHandler("traceback") + + options = PETSc.Options() + # options["ksp_rtol"] = inner_rtol + # options["ksp_atol"] = inner_rtol + options["snes_converged_reason"] = None + options["snes_monitor_short"] = None + + mesh = uw3.meshing.StructuredQuadBox( + elementRes=(int(res), int(res)), minCoords=(-1.0, 0.0), maxCoords=(1.0, 1.0) + ) + + v = uw3.discretisation.MeshVariable("U", mesh, mesh.dim, degree=2) + p = uw3.discretisation.MeshVariable("P", mesh, 1, degree=1) + + stokes = uw3.systems.Stokes(mesh, velocityField=v, pressureField=p) + + stokes.constitutive_model = uw3.constitutive_models.ViscousFlowModel + + ### free slip. + ### note with petsc we always need to provide a vector of correct cardinality. + # stokes.add_dirichlet_bc( (0.,0.), ["Bottom", "Top"], 1 ) # top/bottom: components, function, markers + # stokes.add_dirichlet_bc( (0.,0.), ["Left", "Right"], 0 ) # left/right: components, function, markers + + ### No slip (?) + sol_vel = sympy.Matrix([0, 0]) + + stokes.add_dirichlet_bc( + sol_vel, ["Top", "Bottom"], [0, 1] + ) # top/bottom: components, function, markers + stokes.add_dirichlet_bc( + sol_vel, ["Left", "Right"], [0, 1] + ) # left/right: components, function, markers + + swarm = uw3.swarm.Swarm(mesh=mesh) + material = uw3.swarm.IndexSwarmVariable("M", swarm, indices=4) + swarm.populate(fill_param=swarmGPC) + + blob = numpy.array( + # [[ 0.25, 0.75, 0.1, 1], + # [ 0.45, 0.70, 0.05, 2], + # [ 0.65, 0.60, 0.06, 3], + [[sphereCentre[0], sphereCentre[1], sphereRadius, 1]] + ) + # [ 0.65, 0.20, 0.06, 2], + # [ 0.45, 0.20, 0.12, 3] ]) + + with swarm.access(material): + material.data[...] = 0 + + for i in range(blob.shape[0]): + cx, cy, r, m = blob[i, :] + inside = (swarm.data[:, 0] - cx) ** 2 + ( + swarm.data[:, 1] - cy + ) ** 2 < r**2 + material.data[inside] = m + + tracer = numpy.zeros(shape=(1, 2)) + tracer[:, 0], tracer[:, 1] = x_pos, y_pos + + mat_density = numpy.array([densityBG, densitySphere]) + + density = mat_density[0] * material.sym[0] + mat_density[1] * material.sym[1] + + viscoPlastic_BG = sympy.Min(Cohesion / (2.0 * (stokes._Einv2)), viscBG) + + mat_viscosity = np.array([viscoPlastic_BG, viscSphere]) + + viscosityMat = ( + mat_viscosity[0] * material.sym[0] + mat_viscosity[1] * material.sym[1] + ) + + viscosity = sympy.Max(sympy.Min(viscosityMat, eta_max), eta_min) + + def plot_fig(): + + import numpy as np + import pyvista as pv + import vtk + + pv.global_theme.background = "white" + pv.global_theme.window_size = [750, 750] + pv.global_theme.antialiasing = True + pv.global_theme.jupyter_backend = "trame" + pv.global_theme.smooth_shading = True + + mesh.vtk("tempMsh.vtk") + pvmesh = pv.read("tempMsh.vtk") + + with swarm.access(): + points = numpy.zeros((swarm.data.shape[0], 3)) + points[:, 0] = swarm.data[:, 0] + points[:, 1] = swarm.data[:, 1] + points[:, 2] = 0.0 + + point_cloud = pv.PolyData(points) + + with swarm.access(): + point_cloud.point_data["M"] = material.data.copy() + + with mesh.access(): + vsol = v.data.copy() + + arrow_loc = np.zeros((v.coords.shape[0], 3)) + arrow_loc[:, 0:2] = v.coords[...] + + arrow_length = np.zeros((v.coords.shape[0], 3)) + arrow_length[:, 0:2] = vsol[...] + + pl = pv.Plotter(notebook=True) + + pl.add_mesh(pvmesh, "Black", "wireframe") + + # pl.add_points(point_cloud, color="Black", + # render_points_as_spheres=False, + # point_size=2.5, opacity=0.75) + + pl.add_mesh( + point_cloud, + cmap="coolwarm", + edge_color="Black", + show_edges=False, + scalars="M", + use_transparency=False, + opacity=0.95, + ) + + pl.add_arrows(arrow_loc, arrow_length, mag=5.0, opacity=0.5) + + pl.show(cpos="xy") + + if render: + plot_fig() + + # stokes.constitutive_model = uw3.systems.constitutive_models.ViscousFlowModel(mesh.dim) + # stokes.constitutive_model.material_properties = stokes.constitutive_model.Parameters(viscosity = viscosity ) + + # # stokes.viscosity = viscosity + + # stokes.bodyforce = -1. * density * mesh.N.j + + step = 0 + time = 0.0 + + tSinker = np.zeros(nsteps) + ySinker = np.zeros(nsteps) + + #### initial linear solve + stokes.viscosity = 1.0 + stokes.constitutive_model.material_properties = ( + stokes.constitutive_model.Parameters(viscosity=1.0) + ) + # stokes.bodyforce = -1* 1e-32 * mesh.N.j + stokes.bodyforce = -1 * density * mesh.N.j + # stokes.petsc_options["snes_type"] = "ksponly" + stokes.solve() + + ### add in NL viscosity + stokes.viscosity = viscosity + stokes.constitutive_model.material_properties = ( + stokes.constitutive_model.Parameters(viscosity=viscosity) + ) + stokes.bodyforce = -1 * density * mesh.N.j + + while step < nsteps: + + stokes.solve() + + if uw3.mpi.rank == 0: + ymin = tracer[:, 1].min() + ySinker[step] = ymin + tSinker[step] = time + print( + f"Step: {str(step).rjust(3)}, time: {time:6.2f}, tracer: {ymin:6.2f}" + ) # , vrms {vrms_val:.3e}") + + ### estimate dt + dt = stokes.estimate_dt() + + with swarm.access(): + vel_on_particles = uw3.function.evaluate( + stokes.u.fn, swarm.particle_coordinates.data + ) + + ### advect swarm + with swarm.access(swarm.particle_coordinates): + swarm.particle_coordinates.data[:] += dt * vel_on_particles + + vel_on_tracer = uw3.function.evaluate(stokes.u.fn, tracer) + tracer += dt * vel_on_tracer + + # if MPI.COMM_WORLD.rank==0: + # print('step = {0:6d}; time = {1:.3e}; v_rms = {2:.3e}; height = {3:.3e}' + # .format(step,time,vrms,ymin)) + + step += 1 + time += dt + + if uw3.mpi.rank == 0: + print( + "Initial position: t = {0:.3f}, y = {1:.3f}".format(tSinker[0], ySinker[0]) + ) + print( + "Final position: t = {0:.3f}, y = {1:.3f}".format( + tSinker[nsteps - 1], ySinker[nsteps - 1] + ) + ) + + uw2.utils.matplotlib_inline() + import matplotlib.pyplot as pyplot + + fig = pyplot.figure() + fig.set_size_inches(12, 6) + ax = fig.add_subplot(1, 1, 1) + ax.plot(tSinker, ySinker) + ax.set_xlabel("Time") + ax.set_ylabel("Sinker position") + + # fig1.show() + + if render: + plot_fig() + + return tSinker, ySinker + + + +# %% +tSinker_UW2, ySinker0_UW2, ySinker1_UW2 = uw2_stokesSinker() + +# %% +tSinker_UW3, ySinker_UW3 = uw3_stokesSinker() + +# %% +UW2_vel = (ySinker0_UW2[0] - ySinker0_UW2[-1]) / (tSinker_UW2[0] - tSinker_UW2[-1]) +UW3_vel = (ySinker_UW3[0] - ySinker_UW3[-1]) / (tSinker_UW3[0] - tSinker_UW3[-1]) + + +# %% +print(f"\n\n\n UW2 velocity: {UW2_vel}, UW3 velocity: {UW3_vel/2.} \n\n\n") + +# %% +if uw3.mpi.rank == 0: + import matplotlib.pyplot as pyplot + + fig = pyplot.figure() + fig.set_size_inches(12, 6) + ax = fig.add_subplot(1, 1, 1) + ax.plot(tSinker_UW2, ySinker0_UW2, c="blue", ls="--", label="UW2") + + ax.plot(tSinker_UW2, ySinker1_UW2, c="orange", ls="-.", label="UW2") + + ax.plot(tSinker_UW3, ySinker_UW3, c="red", ls=":", label="UW3") + + ax.legend() + + ax.set_xlabel("Time") + ax.set_ylabel("Sinker position") + + # fig1.show() + diff --git a/main/_sources/Notebooks/Examples-UW-2to3/output/README.md b/main/_sources/Notebooks/Examples-UW-2to3/output/README.md new file mode 100644 index 0000000..afdaad0 --- /dev/null +++ b/main/_sources/Notebooks/Examples-UW-2to3/output/README.md @@ -0,0 +1,3 @@ +# UW2 -> UW3 model outputs + +These files are NOT under version control \ No newline at end of file diff --git a/main/_sources/Notebooks/Examples-Utilities/Ex_Anisotropy.py b/main/_sources/Notebooks/Examples-Utilities/Ex_Anisotropy.py new file mode 100644 index 0000000..11bd68c --- /dev/null +++ b/main/_sources/Notebooks/Examples-Utilities/Ex_Anisotropy.py @@ -0,0 +1,494 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.16.0 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# +# # Anisotropic Constitutive relationships in Underworld (pt 1) +# +# Introduction to how the anisotropic constitutive relationships in Underworld are formulated. +# +# This may be helpful: +# +# +# ![](https://d32ogoqmya1dw8.cloudfront.net/images/NAGTWorkshops/mineralogy/mineral_physics/table_9.v13.png) +# +# +# (Image from: https://serc.carleton.edu/NAGTWorkshops/mineralogy/mineral_physics/tensors.html) +# This will also be helpful: https://en.wikipedia.org/wiki/Elasticity_tensor +# +# ## Incompressibility +# +# If we apply constraints to the deformation, we expect to reduce the number of independent material constants. Incompressibility should reduce the number of *independent* material constants. In an isotropic medium (or a medium with cubic symmetry), incompressibility eliminates the one volumetric material modulus (e.g. the bulk modulus). In general anisotropic media, it is not the case that changes in pressure result in uniform expansion or contraction, and an incompressibility constraint reduces the number of *independent* material constants. In the transversely isotropic case, there are five independent materia constants in general, reducing to four when the material is incompressible. +# +# It is not a given that the stiffness matrix is trivial to construct / meaningful for incompressible anisotropy and there is some discussion here: https://rastgaragah.wordpress.com/2013/03/12/incompressibility-of-linearly-elastic-material/ (identifies the issue but no resolution) and this explains in more detail: https://doi.org/10.1016/S0022-5096(01)00121-1. +# +# +# +# +# + +# + [markdown] magic_args="[markdown]" +# ## Introduction to constitutive models +# +# Constitutive models relate fluxes of a quantity to the gradients of the unknowns. For example, in thermal diffusion, there is a relationship between heat flux and temperature gradients which is known as *Fourier's law* and which involves an empirically determined thermal conductivity. +# +# $$ q_i = k \frac{ \partial T}{\partial x_i} $$ +# +# and in elasticity or fluid flow, we have a relationship between stresses and strain (gradients of the displacements) or strain-rate (gradients of velocity) respectively +# +# $$ \tau_{ij} = \mu \left( \frac{\partial u_i}{\partial x_j} + \frac{\partial u_j}{\partial x_i} \right) $$ +# +# where $\eta$, here, is a material property (elastic shear modulus, viscosity) that we need to determine experimentally. +# +# These material properties can be non-linear (they depend upon the quantities that they relate), and they can also be anisotropic (they vary with the orientation of the material). In the latter case, the material property is described through a *consitutive tensor*. For example: +# +# $$ q_i = k_{ij} \frac{ \partial T}{\partial x_j} $$ +# +# or +# +# $$ \tau_{ij} = \mu_{ijkl} \left( \frac{\partial u_k}{\partial x_l} + \frac{\partial u_l}{\partial x_k} \right) $$ + +# + [markdown] magic_args="[markdown]" +# ## Constitutive tensors +# +# In Underworld, the underlying implementation of any consitutive relationship is through the appropriate tensor representation, allowing for a very general formulation for most problems. The constitutive tensors are described symbolically using `sympy` expressions. For scalar equations, the rank 2 constitutive tensor ($k_{ij}$) can be expressed as a matrix without any loss of generality. +# +# For vector equations, the constitutive tensor is of rank 4, but it is common in the engineering / finite element literature to transform the rank 4 tensor to a matrix representation. This has some benefits in terms of legibility and, perhaps, results in common code patterns across the formulation for scalar equations. +# +# We can write the stress and strain rate tensors ($\tau_{ij}$ and $\dot\epsilon_{ij}$ respectively) as +# +# $$ \tau_I = \left( \tau_{00}, \tau_{11}, \tau_{22}, \tau_{12}, \tau_{02}, \tau_{01} \right) $$ +# +# $$ \dot\varepsilon_I = \left( \dot\varepsilon_{00}, \dot\varepsilon_{11}, \dot\varepsilon_{22}, \dot\varepsilon_{12}, \dot\varepsilon_{02}, \dot\varepsilon_{01} \right) $$ +# +# where $I$ indexes $\{i,j\}$ in a specified order and symmetry of the tensor implies only six (in 3D) unique entries in the nine entries of a general tensor. With these definitions, the constitutive tensor can be written as $C_{IJ}$ where $I$, and $J$ range over the independent degrees of freedom in stress and strain (rate). This is a re-writing of the full tensor, with the necessary symmetry required to respect the symmetry of $\tau$ and $\dot\varepsilon$ +# +# The simplified notation comes at a cost. For example, $\tau_I \dot\varepsilon_I$ is not equivalent to the tensor inner produce $\tau_{ij} \dot\varepsilon_{ij}$, and $C_{IJ}$ does not transform correctly for a rotation of coordinates. +# +# However, a modest transformation of the matrix equations is helpful: +# +# $$ \tau^*_{I} = C^*_{IJ} \varepsilon^*_{J} $$ +# +# Where +# +# $$\tau^*_{I} = P_{IJ} \tau^*_J$$ +# +# $$\varepsilon^*_I = P_{IJ} \varepsilon^*_J$$ +# +# $$C^*_{IJ} = P_{IK} C_{KL} P_{LJ}$$ +# +# $\mathbf{P}$ is the scaling matrix +# +# $$P = \left[\begin{matrix}1 & 0 & 0 & 0 & 0 & 0\\0 & 1 & 0 & 0 & 0 & 0\\0 & 0 & 1 & 0 & 0 & 0\\0 & 0 & 0 & \sqrt{2} & 0 & 0\\0 & 0 & 0 & 0 & \sqrt{2} & 0\\0 & 0 & 0 & 0 & 0 & \sqrt{2}\end{matrix}\right]$$ +# +# In this form, known as the Mandel notation, $\tau^*_I\varepsilon^*_I \equiv \tau_{ij} \varepsilon_{ij}$, and the fourth order identity matrix: +# +# $$I_{ijkl} = \frac{1}{2} \left( \delta_{ij}\delta_{kj} + \delta_{kl}\delta_{il} \right)$$ +# +# transforms to +# +# $$I_{IJ} = \delta_{IJ}$$ +# +# +# +# + +# + [markdown] magic_args="[markdown]" +# ## Mandel form, sympy tensorial form, Voigt form +# +# The rank 4 tensor form of the constitutive equations, $c_{ijkl}$, has the following representation in `sympy`: +# +# $$\left[\begin{matrix}\left[\begin{matrix}c_{0 0 0 0} & c_{0 0 0 1}\\c_{0 0 1 0} & c_{0 0 1 1}\end{matrix}\right] & \left[\begin{matrix}c_{0 1 0 0} & c_{0 1 0 1}\\c_{0 1 1 0} & c_{0 1 1 1}\end{matrix}\right]\\\left[\begin{matrix}c_{1 0 0 0} & c_{1 0 0 1}\\c_{1 0 1 0} & c_{1 0 1 1}\end{matrix}\right] & \left[\begin{matrix}c_{1 1 0 0} & c_{1 1 0 1}\\c_{1 1 1 0} & c_{1 1 1 1}\end{matrix}\right]\end{matrix}\right]$$ +# +# and the inner product $\tau_{ij} = c_{ijkl} \varepsilon_{kl} $ is written +# +# ```python +# tau = sympy.tensorcontraction( +# sympy.tensorcontraction( +# sympy.tensorproduct(c, epsilon),(3,5)),(2,3)) +# ``` +# +# However, the `sympy.Matrix` module allows a much simpler expression +# +# ```python +# tau_star = C_star * epsilon_star +# tau = P.inv() * tau_star +# ``` +# +# which we adopt in `underworld` for the display and manipulation of constitutive tensors. +# +# ### Voigt form +# +# Computation of the stress tensor using $\tau^*_I = C^*_{IJ}\varepsilon^*_J$ is equivalent to the following +# +# $$ \mathbf{P} \mathbf{\tau} = \mathbf{P} \mathbf{C} \mathbf{P} \cdot \mathbf{P} \mathbf{\varepsilon} $$ +# +# multiply by $\mathbf{P}^{-1} $ and collecting terms: +# +# $$ \mathbf{\tau} = \mathbf{C} \mathbf{P}^2 \mathbf{\varepsilon} $$ +# +# This is the Voigt form of the constitutive equations and is generally what you will find in a finite element textbook. The Voigt form of the constitutive matrix is the rearrangement of the rank 4 constitutive tensor (with no scaling), the strain rate vector is usually combined with $\mathbf{P}^2$, and the stress vector is raw. For example: +# +# +# $$ \left[\begin{matrix}\tau_{0 0}\\\tau_{1 1}\\\tau_{0 1}\end{matrix}\right] = +# \left[\begin{matrix}\eta & 0 & 0\\0 & \eta & 0\\0 & 0 & \frac{\eta}{2}\end{matrix}\right] \left[\begin{matrix}\dot\varepsilon_{0 0}\\\dot\varepsilon_{1 1}\\2 \dot\varepsilon_{0 1}\end{matrix}\right] +# $$ +# +# A full discussion of this can be found in Brannon (2018) and Helnwein (2001). +# +# +# ### References. +# +# Brannon, R. (2018). Rotation Reflection and Frame Changes Orthogonal tensors in computational engineering mechanics. IOP Publishing. https://doi.org/10.1088/978-0-7503-1454-1 +# +# Helnwein, P. (2001). Some remarks on the compressed matrix representation of symmetric second-order and fourth-order tensors. Computer Methods in Applied Mechanics and Engineering, 190(22–23), 2753–2770. https://doi.org/10.1016/S0045-7825(00)00263-2 +# +# +# +# - + +# ## Rotations +# +# The expressions for our anisotropic tensors are provided in a Canonical reference frame which exploits the symmetry of the material to present a compact form of the constitutive tensors. In a general orientation, a rotation tensor $\cal R$ is required to transform to / from this canonical frame. For isotropic materials, $\cal R$ is the identity matrix. For cubic materials, a single angle describes the offset of the two frames, and for transversely isotropic materials, we need to define a rotation matrix that aligns the normal to the symmetry plane with the vertical axis in the canonical frame. These examples have fewer degrees of freedom than a full rotation matrix and this fact can be used to simplify the general (rotated) form of the constitutive tensors. +# +# +# Define ${\cal R}_{ij}$ as the rotation matrix that maps $x$ onto the $x' $ coordiate system, i.e. +# +# +# $\displaystyle a_i' = {\cal R}_{ij}\,a_j,$, for vectors, +# +# which also has this property: +# +# $\displaystyle {\cal R}_{ki}\,{\cal R}_{kj } = \delta_{ij}.$ +# +# Second order tensors transform as follows: +# +# $\displaystyle a_{ij}' = {\cal R}_{ik}\,{\cal R}_{jl}\,a_{kl},$ +# +# and for higher rank tensors, we just continue ... +# +# $\displaystyle a_{ijk}' = {\cal R}_{il}\,{\cal R}_{jm}\,{\cal R}_{kn}\,a_{lmn}.$ +# +# In `underworld` tensor rotation is provided for rank 2 and rank 4 tensors by `uw.maths.tensor.tensor_rotation(R, tensor_expression)` +# +# ### Example +# +# +# First we define the rotation for the transversely isotropic case which depends on the normal vector to the symmetry plane. +# + +# A rotation matrix for a transversely isotropic medium is defined by specifying the normal of the symmetry plane ($\hat{\mathbf{n}} = \{ n_0, n_1, n_2\} $). +# The other orientations are arbitrary, so we simply derive them from $\hat{\mathbf{n}}$ - one vector specified to be perpendicular in the horizontal plane and the third vector is then found from their cross product ($\hat{\mathbf{s}}$ and $\hat{\mathbf{t}}$ respectively) +# +# $$\cal{R} = \left[\begin{matrix}\frac{n_{1}}{\sqrt{n_{0}^{2} + n_{1}^{2}}} & - \frac{n_{0} n_{2}}{\sqrt{n_{0}^{2} + n_{1}^{2}}} & n_{0}\\- \frac{n_{0}}{\sqrt{n_{0}^{2} + n_{1}^{2}}} & - \frac{n_{1} n_{2}}{\sqrt{n_{0}^{2} + n_{1}^{2}}} & n_{1}\\0 & \frac{n_{0}^{2}}{\sqrt{n_{0}^{2} + n_{1}^{2}}} + \frac{n_{1}^{2}}{\sqrt{n_{0}^{2} + n_{1}^{2}}} & n_{2}\end{matrix}\right]$$ + +# + +## uw / sympy implementation + +import underworld3 as uw +import sympy + +n = sympy.Matrix(sympy.symarray("n",(3,))) + +# Or give a specific value (just specify a vector, then normalise) +# n = sympy.Matrix([1,1,1]) +# n /= sympy.sqrt(n.dot(n)) + +if n[0] == 0: + s = sympy.Matrix([1,0,0]) + t = sympy.Matrix([0,1,0]) + R = sympy.eye(3) + +else: + s = sympy.Matrix((n[1] , -n[0], 0 )) + s /= sympy.sqrt(s.dot(s)) + t = -n.cross(s) # complete the coordinate triad + R = sympy.BlockMatrix((s,t,n)).as_explicit() + +display(R) + +# print(sympy.latex(R, mode='plain')) +# - + +# ## Validation +# +# A simple check on this is to rotate the isotropic constitutive tensor and validate that +# it is invariant under rotation. +# +# $$C_{IJ} = \left[\begin{matrix}2 \eta_{0} & 0 & 0 & 0 & 0 & 0\\0 & 2 \eta_{0} & 0 & 0 & 0 & 0\\0 & 0 & 2 \eta_{0} & 0 & 0 & 0\\0 & 0 & 0 & 2 \eta_{0} & 0 & 0\\0 & 0 & 0 & 0 & 2 \eta_{0} & 0\\0 & 0 & 0 & 0 & 0 & 2 \eta_{0}\end{matrix}\right]$$ +# +# Noting that the rotation of the Mandel or Voigt constitutive matrices is complicated by the $\mathbf{P}$ scaling matrices, we compute rotations on the rank 4 tensor, $c_{ijkl}$ and transform to the matrix forms as required. We denote the rotation from $\{IJ\}$ coordinates to $\{I'J'\}$ as +# +# $$C_{I'J'} = {\cal R}[C_{IJ}]$$ +# +# The rotated constitutive model has the following form: +# +# $$C_{I'J'} = \left[\begin{matrix}\frac{2 \eta_{0} \left(n_{0}^{2} n_{2}^{2} + n_{0}^{2} \left(n_{0}^{2} + n_{1}^{2}\right) + n_{1}^{2}\right)^{2}}{\left(n_{0}^{2} + n_{1}^{2}\right)^{2}} & \frac{2 \eta_{0} n_{0}^{2} n_{1}^{2} \left(n_{0}^{2} + n_{1}^{2} + n_{2}^{2} - 1\right)^{2}}{\left(n_{0}^{2} + n_{1}^{2}\right)^{2}} & 0 & 0 & 0 & \frac{2 \sqrt{2} \eta_{0} n_{0} n_{1} \left(n_{0}^{6} + 2 n_{0}^{4} n_{1}^{2} + 2 n_{0}^{4} n_{2}^{2} - n_{0}^{4} + n_{0}^{2} n_{1}^{4} + 2 n_{0}^{2} n_{1}^{2} n_{2}^{2} + n_{0}^{2} n_{2}^{4} - n_{0}^{2} n_{2}^{2} + n_{1}^{4} + n_{1}^{2} n_{2}^{2} - n_{1}^{2}\right)}{n_{0}^{4} + 2 n_{0}^{2} n_{1}^{2} + n_{1}^{4}}\\\frac{2 \eta_{0} n_{0}^{2} n_{1}^{2} \left(n_{0}^{2} + n_{1}^{2} + n_{2}^{2} - 1\right)^{2}}{\left(n_{0}^{2} + n_{1}^{2}\right)^{2}} & \frac{2 \eta_{0} \left(n_{0}^{2} + n_{1}^{2} n_{2}^{2} + n_{1}^{2} \left(n_{0}^{2} + n_{1}^{2}\right)\right)^{2}}{\left(n_{0}^{2} + n_{1}^{2}\right)^{2}} & 0 & 0 & 0 & \frac{2 \sqrt{2} \eta_{0} n_{0} n_{1} \left(n_{0}^{4} n_{1}^{2} + n_{0}^{4} + 2 n_{0}^{2} n_{1}^{4} + 2 n_{0}^{2} n_{1}^{2} n_{2}^{2} + n_{0}^{2} n_{2}^{2} - n_{0}^{2} + n_{1}^{6} + 2 n_{1}^{4} n_{2}^{2} - n_{1}^{4} + n_{1}^{2} n_{2}^{4} - n_{1}^{2} n_{2}^{2}\right)}{n_{0}^{4} + 2 n_{0}^{2} n_{1}^{2} + n_{1}^{4}}\\0 & 0 & 2 \eta_{0} \left(n_{0}^{2} + n_{1}^{2} + n_{2}^{2}\right)^{2} & 0 & 0 & 0\\0 & 0 & 0 & \frac{2 \eta_{0} \left(n_{0}^{2} + n_{1}^{2} + n_{2}^{2}\right) \left(n_{0}^{2} + n_{1}^{2} n_{2}^{2} + n_{1}^{2} \left(n_{0}^{2} + n_{1}^{2}\right)\right)}{n_{0}^{2} + n_{1}^{2}} & \frac{2 \eta_{0} n_{0} n_{1} \left(n_{0}^{2} + n_{1}^{2} + n_{2}^{2}\right) \left(n_{0}^{2} + n_{1}^{2} + n_{2}^{2} - 1\right)}{n_{0}^{2} + n_{1}^{2}} & 0\\0 & 0 & 0 & \frac{2 \eta_{0} n_{0} n_{1} \left(n_{0}^{2} + n_{1}^{2} + n_{2}^{2}\right) \left(n_{0}^{2} + n_{1}^{2} + n_{2}^{2} - 1\right)}{n_{0}^{2} + n_{1}^{2}} & \frac{2 \eta_{0} \left(n_{0}^{2} + n_{1}^{2} + n_{2}^{2}\right) \left(n_{0}^{2} n_{2}^{2} + n_{0}^{2} \left(n_{0}^{2} + n_{1}^{2}\right) + n_{1}^{2}\right)}{n_{0}^{2} + n_{1}^{2}} & 0\\\frac{2 \sqrt{2} \eta_{0} n_{0} n_{1} \left(n_{0}^{2} n_{2}^{2} + n_{0}^{2} \left(n_{0}^{2} + n_{1}^{2}\right) + n_{1}^{2}\right) \left(n_{0}^{2} + n_{1}^{2} + n_{2}^{2} - 1\right)}{\left(n_{0}^{2} + n_{1}^{2}\right)^{2}} & \frac{2 \sqrt{2} \eta_{0} n_{0} n_{1} \left(n_{0}^{2} + n_{1}^{2} n_{2}^{2} + n_{1}^{2} \left(n_{0}^{2} + n_{1}^{2}\right)\right) \left(n_{0}^{2} + n_{1}^{2} + n_{2}^{2} - 1\right)}{\left(n_{0}^{2} + n_{1}^{2}\right)^{2}} & 0 & 0 & 0 & \frac{2 \eta_{0} \left(n_{0}^{2} n_{2}^{2} \left(n_{0}^{2} + 2 n_{1}^{2} n_{2}^{2} + 2 n_{1}^{2} \left(n_{0}^{2} + n_{1}^{2}\right) - n_{1}^{2}\right) + n_{0}^{2} \left(n_{0}^{2} + n_{1}^{2}\right) \left(n_{0}^{2} + 2 n_{1}^{2} n_{2}^{2} + 2 n_{1}^{2} \left(n_{0}^{2} + n_{1}^{2}\right) - n_{1}^{2}\right) + n_{1}^{2} \cdot \left(2 n_{0}^{2} + n_{2}^{2} \left(- n_{0}^{2} + n_{1}^{2}\right) - \left(n_{0}^{2} - n_{1}^{2}\right) \left(n_{0}^{2} + n_{1}^{2}\right)\right)\right)}{\left(n_{0}^{2} + n_{1}^{2}\right)^{2}}\end{matrix}\right]$$ +# +# However, $\{n_0, n_1, n_2\}$ are not independent because $\hat{\mathbf{n}}$ is a unit vector. If we add this information and simplify, we recover the isotropic form of $C$ + +# + +# construct a symbolic, isotropic matrix (Mandel form) + +eta0 = sympy.symbols("\eta_0") +C_IJm = 2 * sympy.Matrix.diag([eta0]*6) +display(C_IJm) + +## Rotate the matrix + +c_ijkl = uw.maths.tensor.mandel_to_rank4(C_IJm, 3) +C_IJv = uw.maths.tensor.rank4_to_voigt(c_ijkl, 3) +c_ijkl_R = sympy.simplify(uw.maths.tensor.tensor_rotation(R, c_ijkl)) +C_IJm_R = sympy.simplify(uw.maths.tensor.rank4_to_mandel(c_ijkl_R, 3)) +display(C_IJm_R) + +## Is this really invariant under rotation ?? +## Have to do some manipulation to identify the unit-vector component relationships + +C_IJm_R_s1 = C_IJm_R.subs(n[0]**2+n[1]**2+n[2]**2,1) +C_IJm_R_s2 = C_IJm_R_s1.subs(n[0]**2+n[1]**2, 1-n[2]**2).applyfunc(sympy.factor) +C_IJm_R_s2.subs(n[0]**2+n[1]**2, 1-n[2]**2).simplify() + +# - + +# ## Muhlhaus / Moresi transversely isotropic tensor +# +# The Muhlhaus / Moresi transversely isotropic consitituve model is designed to model +# materials that have a single (usually) weak plane (e.g. an embedded fault). +# +# The constitutive model is +# +# $$\left[\begin{matrix}2 \eta_{0} & 0 & 0 & 0 & 0 & 0\\0 & 2 \eta_{0} & 0 & 0 & 0 & 0\\0 & 0 & 2 \eta_{0} & 0 & 0 & 0\\0 & 0 & 0 & - 2 \Delta\eta + 2 \eta_{0} & 0 & 0\\0 & 0 & 0 & 0 & - 2 \Delta\eta + 2 \eta_{0} & 0\\0 & 0 & 0 & 0 & 0 & 2 \eta_{0}\end{matrix}\right]$$ +# +# where $\Delta\eta$ represents the change (usually reduction) in viscosity. +# +# Rotation using $\cal R$ as defined from the normal to the weak plane (above) gives (Mandel form): +# +# $$\left[\begin{matrix}- \frac{4 \Delta\eta n_{0}^{2} \left(n_{0}^{2} n_{2}^{2} + n_{1}^{2}\right)}{n_{0}^{2} + n_{1}^{2}} + 2 \eta_{0} & \frac{4 \Delta\eta n_{0}^{2} n_{1}^{2} \cdot \left(1 - n_{2}^{2}\right)}{n_{0}^{2} + n_{1}^{2}} & 4 \Delta\eta n_{0}^{2} n_{2}^{2} & \frac{2 \sqrt{2} \Delta\eta n_{0}^{2} n_{1} n_{2} \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2} + 1\right)}{n_{0}^{2} + n_{1}^{2}} & \frac{2 \sqrt{2} \Delta\eta n_{0} n_{2} \left(n_{0}^{2} \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2}\right) - n_{1}^{2}\right)}{n_{0}^{2} + n_{1}^{2}} & \frac{2 \sqrt{2} \Delta\eta n_{0} n_{1} \left(- 2 n_{0}^{2} n_{2}^{2} + n_{0}^{2} - n_{1}^{2}\right)}{n_{0}^{2} + n_{1}^{2}}\\\frac{4 \Delta\eta n_{0}^{2} n_{1}^{2} \cdot \left(1 - n_{2}^{2}\right)}{n_{0}^{2} + n_{1}^{2}} & - \frac{4 \Delta\eta n_{1}^{2} \left(n_{0}^{2} + n_{1}^{2} n_{2}^{2}\right)}{n_{0}^{2} + n_{1}^{2}} + 2 \eta_{0} & 4 \Delta\eta n_{1}^{2} n_{2}^{2} & - \frac{2 \sqrt{2} \Delta\eta n_{1} n_{2} \left(n_{0}^{2} - n_{1}^{2} \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2}\right)\right)}{n_{0}^{2} + n_{1}^{2}} & \frac{2 \sqrt{2} \Delta\eta n_{0} n_{1}^{2} n_{2} \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2} + 1\right)}{n_{0}^{2} + n_{1}^{2}} & \frac{2 \sqrt{2} \Delta\eta n_{0} n_{1} \left(- n_{0}^{2} - 2 n_{1}^{2} n_{2}^{2} + n_{1}^{2}\right)}{n_{0}^{2} + n_{1}^{2}}\\4 \Delta\eta n_{0}^{2} n_{2}^{2} & 4 \Delta\eta n_{1}^{2} n_{2}^{2} & - 4 \Delta\eta n_{2}^{2} \left(n_{0}^{2} + n_{1}^{2}\right) + 2 \eta_{0} & 2 \sqrt{2} \Delta\eta n_{1} n_{2} \left(- n_{0}^{2} - n_{1}^{2} + n_{2}^{2}\right) & 2 \sqrt{2} \Delta\eta n_{0} n_{2} \left(- n_{0}^{2} - n_{1}^{2} + n_{2}^{2}\right) & 4 \sqrt{2} \Delta\eta n_{0} n_{1} n_{2}^{2}\\\frac{2 \sqrt{2} \Delta\eta n_{0}^{2} n_{1} n_{2} \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2} + 1\right)}{n_{0}^{2} + n_{1}^{2}} & \frac{2 \sqrt{2} \Delta\eta n_{1} n_{2} \left(n_{0}^{2} n_{1}^{2} - n_{0}^{2} + n_{1}^{4} - n_{1}^{2} n_{2}^{2}\right)}{n_{0}^{2} + n_{1}^{2}} & 2 \sqrt{2} \Delta\eta n_{1} n_{2} \left(- n_{0}^{2} - n_{1}^{2} + n_{2}^{2}\right) & - \frac{2 \Delta\eta \left(n_{0}^{2} n_{2}^{2} - n_{1}^{2} n_{2}^{2} \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2}\right) + n_{1}^{2} \left(n_{0}^{2} + n_{1}^{2}\right) \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2}\right)\right)}{n_{0}^{2} + n_{1}^{2}} + 2 \eta_{0} & \frac{2 \Delta\eta n_{0} n_{1} \left(n_{2}^{2} \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2}\right) + n_{2}^{2} - \left(n_{0}^{2} + n_{1}^{2}\right) \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2}\right)\right)}{n_{0}^{2} + n_{1}^{2}} & \frac{2 \Delta\eta n_{0} n_{2} \cdot \left(2 n_{0}^{2} n_{1}^{2} - n_{0}^{2} + 2 n_{1}^{4} - 2 n_{1}^{2} n_{2}^{2} + n_{1}^{2}\right)}{n_{0}^{2} + n_{1}^{2}}\\\frac{2 \sqrt{2} \Delta\eta n_{0} n_{2} \left(n_{0}^{4} + n_{0}^{2} n_{1}^{2} - n_{0}^{2} n_{2}^{2} - n_{1}^{2}\right)}{n_{0}^{2} + n_{1}^{2}} & \frac{2 \sqrt{2} \Delta\eta n_{0} n_{1}^{2} n_{2} \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2} + 1\right)}{n_{0}^{2} + n_{1}^{2}} & 2 \sqrt{2} \Delta\eta n_{0} n_{2} \left(- n_{0}^{2} - n_{1}^{2} + n_{2}^{2}\right) & \frac{2 \Delta\eta n_{0} n_{1} \left(n_{2}^{2} \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2}\right) + n_{2}^{2} - \left(n_{0}^{2} + n_{1}^{2}\right) \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2}\right)\right)}{n_{0}^{2} + n_{1}^{2}} & - \frac{2 \Delta\eta \left(- n_{0}^{2} n_{2}^{2} \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2}\right) + n_{0}^{2} \left(n_{0}^{2} + n_{1}^{2}\right) \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2}\right) + n_{1}^{2} n_{2}^{2}\right)}{n_{0}^{2} + n_{1}^{2}} + 2 \eta_{0} & \frac{2 \Delta\eta n_{1} n_{2} \cdot \left(2 n_{0}^{4} + 2 n_{0}^{2} n_{1}^{2} - 2 n_{0}^{2} n_{2}^{2} + n_{0}^{2} - n_{1}^{2}\right)}{n_{0}^{2} + n_{1}^{2}}\\\frac{2 \sqrt{2} \Delta\eta n_{0} n_{1} \left(- 2 n_{0}^{2} n_{2}^{2} + n_{0}^{2} - n_{1}^{2}\right)}{n_{0}^{2} + n_{1}^{2}} & \frac{2 \sqrt{2} \Delta\eta n_{0} n_{1} \left(- n_{0}^{2} - 2 n_{1}^{2} n_{2}^{2} + n_{1}^{2}\right)}{n_{0}^{2} + n_{1}^{2}} & 4 \sqrt{2} \Delta\eta n_{0} n_{1} n_{2}^{2} & \frac{2 \Delta\eta n_{0} n_{2} \left(- n_{0}^{2} + 2 n_{1}^{2} \left(n_{0}^{2} + n_{1}^{2} - n_{2}^{2}\right) + n_{1}^{2}\right)}{n_{0}^{2} + n_{1}^{2}} & \frac{2 \Delta\eta n_{1} n_{2} \cdot \left(2 n_{0}^{4} + 2 n_{0}^{2} n_{1}^{2} - 2 n_{0}^{2} n_{2}^{2} + n_{0}^{2} - n_{1}^{2}\right)}{n_{0}^{2} + n_{1}^{2}} & \frac{2 \Delta\eta \left(- n_{0}^{4} - 4 n_{0}^{2} n_{1}^{2} n_{2}^{2} + 2 n_{0}^{2} n_{1}^{2} - n_{1}^{4}\right)}{n_{0}^{2} + n_{1}^{2}} + 2 \eta_{0}\end{matrix}\right]$$ +# +# and we can easily demonstrate that this collapses back to the isotropic form if $\Delta\eta \leftarrow 0$. We can also show this is equivalent to the alternate expression for the tensor provided in the original papers (an exercise for the reader !!). +# +# The `underworld` implementation is: + +# + +# Muhlhaus definition of C_IJ (mandel form) + +eta1 = sympy.symbols("\eta_1") +delta_eta = sympy.symbols("\Delta\eta") + +C_ijkl_MM = uw.maths.tensor.rank4_identity(3) * 0 +C_IJm_MM = uw.maths.tensor.rank4_to_mandel(C_ijkl_MM, 3) +C_IJm_MM[0,0] = 2*eta0 +C_IJm_MM[1,1] = 2*eta0 +C_IJm_MM[2,2] = 2*eta0 +C_IJm_MM[3,3] = 2*(eta0-delta_eta) # yz +C_IJm_MM[4,4] = 2*(eta0-delta_eta) # xz +C_IJm_MM[5,5] = 2*eta0 # xy + +display(C_IJm_MM) + +## We know that the isotropic part is invariant under rotation, so we only need to +## examine the non-isotropic part. + +C_ijkl_MM = uw.maths.tensor.mandel_to_rank4(C_IJm_MM - C_IJm , 3) +C_ijkl_MM_R = sympy.simplify(uw.maths.tensor.tensor_rotation(R, C_ijkl_MM)) + +C_IJm_MM_R = sympy.simplify(uw.maths.tensor.rank4_to_mandel(C_ijkl_MM_R, 3)) + C_IJm +C_IJv_MM_R = sympy.simplify(uw.maths.tensor.rank4_to_voigt(C_ijkl_MM_R, 3)) + C_IJv + +display(C_IJm_MM_R) + +# Check what happens if we set delta eta to zero +C_IJm_MM_iso = C_IJm_MM_R.subs(delta_eta,0).applyfunc(sympy.factor).subs(n[0]**2+n[1]**2, 1-n[2]**2).simplify() +display(C_IJm_MM_iso) + +# - + + +# ## Han & Wahr (full transverse isotropic tensor) +# +# In the Han & Wahr paper, the expression for incompressible transverse-isotropy is as follows +# +# $$\left[\begin{matrix}2 \eta_{0} + \mu_{0} & \mu_{0} & 0 & 0 & 0 & 0\\\mu_{0} & 2 \eta_{0} + \mu_{0} & 0 & 0 & 0 & 0\\0 & 0 & - 2 \Delta\eta + 2 \eta_{0} + \mu_{1} & 0 & 0 & 0\\0 & 0 & 0 & - 2 \Delta\eta + 2 \eta_{0} & 0 & 0\\0 & 0 & 0 & 0 & - 2 \Delta\eta + 2 \eta_{0} & 0\\0 & 0 & 0 & 0 & 0 & 2 \eta_{0}\end{matrix}\right]$$ +# +# Note that the notation differs from their paper. I have replaced their $\nu1, \nu2$ with $\eta0, \eta1$ to be consistent with the forms defined above. I have replaced their $\eta$ with $\mu$ to avoid the confusion that results from the first change. +# +# Applying the rotation, $\cal R$ and attempting to coerce `sympy` to simplify the constitutive matrix: +# +# $$\left[\begin{matrix}\frac{2 \Delta\eta n_{0}^{4} n_{2}^{4} - 2 \Delta\eta n_{0}^{4} + 4 \Delta\eta n_{0}^{2} n_{1}^{2} n_{2}^{2} - 4 \Delta\eta n_{0}^{2} n_{1}^{2} + 2 \eta_{0} n_{2}^{4} - 4 \eta_{0} n_{2}^{2} + 2 \eta_{0} + \mu_{0} n_{0}^{4} n_{2}^{4} + 2 \mu_{0} n_{0}^{2} n_{1}^{2} n_{2}^{2} + \mu_{0} n_{1}^{4} + \mu_{1} n_{0}^{4} n_{2}^{4} - 2 \mu_{1} n_{0}^{4} n_{2}^{2} + \mu_{1} n_{0}^{4}}{\left(n_{2} - 1\right)^{2} \left(n_{2} + 1\right)^{2}} & \frac{2 \Delta\eta n_{0}^{2} n_{1}^{2} n_{2}^{4} - 4 \Delta\eta n_{0}^{2} n_{1}^{2} n_{2}^{2} + 2 \Delta\eta n_{0}^{2} n_{1}^{2} + \mu_{0} n_{0}^{4} n_{2}^{2} + \mu_{0} n_{0}^{2} n_{1}^{2} n_{2}^{4} + \mu_{0} n_{0}^{2} n_{1}^{2} + \mu_{0} n_{1}^{4} n_{2}^{2} + \mu_{1} n_{0}^{2} n_{1}^{2} n_{2}^{4} - 2 \mu_{1} n_{0}^{2} n_{1}^{2} n_{2}^{2} + \mu_{1} n_{0}^{2} n_{1}^{2}}{\left(n_{2} - 1\right)^{2} \left(n_{2} + 1\right)^{2}} & 2 \Delta\eta n_{0}^{2} n_{2}^{2} + \mu_{0} n_{0}^{2} n_{2}^{2} + \mu_{0} n_{1}^{2} + \mu_{1} n_{0}^{2} n_{2}^{2} & \frac{\sqrt{2} n_{1} n_{2} \cdot \left(2 \Delta\eta n_{0}^{2} n_{2}^{2} - 2 \Delta\eta n_{0}^{2} + \mu_{0} n_{0}^{2} n_{2}^{2} + \mu_{0} n_{1}^{2} - \mu_{1} n_{0}^{4} - \mu_{1} n_{0}^{2} n_{1}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & \frac{\sqrt{2} n_{0} n_{2} \cdot \left(2 \Delta\eta n_{0}^{2} n_{2}^{2} + 2 \Delta\eta n_{1}^{2} + \mu_{0} n_{0}^{2} n_{2}^{2} + \mu_{0} n_{1}^{2} - \mu_{1} n_{0}^{4} - \mu_{1} n_{0}^{2} n_{1}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & \frac{\sqrt{2} n_{0} n_{1} \cdot \left(2 \Delta\eta n_{0}^{2} n_{2}^{2} + 2 \Delta\eta n_{1}^{2} + \mu_{0} n_{0}^{2} n_{2}^{2} + \mu_{0} n_{1}^{2} + \mu_{1} n_{0}^{2} n_{2}^{2} - \mu_{1} n_{0}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)}\\\frac{2 \Delta\eta n_{0}^{2} n_{1}^{2} n_{2}^{4} - 4 \Delta\eta n_{0}^{2} n_{1}^{2} n_{2}^{2} + 2 \Delta\eta n_{0}^{2} n_{1}^{2} + \mu_{0} n_{0}^{4} n_{2}^{2} + \mu_{0} n_{0}^{2} n_{1}^{2} n_{2}^{4} + \mu_{0} n_{0}^{2} n_{1}^{2} + \mu_{0} n_{1}^{4} n_{2}^{2} + \mu_{1} n_{0}^{2} n_{1}^{2} n_{2}^{4} - 2 \mu_{1} n_{0}^{2} n_{1}^{2} n_{2}^{2} + \mu_{1} n_{0}^{2} n_{1}^{2}}{\left(n_{2} - 1\right)^{2} \left(n_{2} + 1\right)^{2}} & \frac{4 \Delta\eta n_{0}^{2} n_{1}^{2} n_{2}^{2} - 4 \Delta\eta n_{0}^{2} n_{1}^{2} + 2 \Delta\eta n_{1}^{4} n_{2}^{4} - 2 \Delta\eta n_{1}^{4} + 2 \eta_{0} n_{2}^{4} - 4 \eta_{0} n_{2}^{2} + 2 \eta_{0} + \mu_{0} n_{0}^{4} + 2 \mu_{0} n_{0}^{2} n_{1}^{2} n_{2}^{2} + \mu_{0} n_{1}^{4} n_{2}^{4} + \mu_{1} n_{1}^{4} n_{2}^{4} - 2 \mu_{1} n_{1}^{4} n_{2}^{2} + \mu_{1} n_{1}^{4}}{\left(n_{2} - 1\right)^{2} \left(n_{2} + 1\right)^{2}} & 2 \Delta\eta n_{1}^{2} n_{2}^{2} + \mu_{0} n_{0}^{2} + \mu_{0} n_{1}^{2} n_{2}^{2} + \mu_{1} n_{1}^{2} n_{2}^{2} & \frac{\sqrt{2} n_{1} n_{2} \cdot \left(2 \Delta\eta n_{0}^{2} + 2 \Delta\eta n_{1}^{2} n_{2}^{2} + \mu_{0} n_{0}^{2} + \mu_{0} n_{1}^{2} n_{2}^{2} - \mu_{1} n_{0}^{2} n_{1}^{2} - \mu_{1} n_{1}^{4}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & \frac{\sqrt{2} n_{0} n_{2} \cdot \left(2 \Delta\eta n_{1}^{2} n_{2}^{2} - 2 \Delta\eta n_{1}^{2} + \mu_{0} n_{0}^{2} + \mu_{0} n_{1}^{2} n_{2}^{2} - \mu_{1} n_{0}^{2} n_{1}^{2} - \mu_{1} n_{1}^{4}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & \frac{\sqrt{2} n_{0} n_{1} \cdot \left(2 \Delta\eta n_{0}^{2} + 2 \Delta\eta n_{1}^{2} n_{2}^{2} + \mu_{0} n_{0}^{2} + \mu_{0} n_{1}^{2} n_{2}^{2} + \mu_{1} n_{1}^{2} n_{2}^{2} - \mu_{1} n_{1}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)}\\2 \Delta\eta n_{0}^{2} n_{2}^{2} + \mu_{0} n_{0}^{2} n_{2}^{2} + \mu_{0} n_{1}^{2} + \mu_{1} n_{0}^{2} n_{2}^{2} & 2 \Delta\eta n_{1}^{2} n_{2}^{2} + \mu_{0} n_{0}^{2} + \mu_{0} n_{1}^{2} n_{2}^{2} + \mu_{1} n_{1}^{2} n_{2}^{2} & 2 \Delta\eta n_{2}^{4} - 4 \Delta\eta n_{2}^{2} + 2 \eta_{0} + \mu_{0} n_{2}^{4} - 2 \mu_{0} n_{2}^{2} + \mu_{0} + \mu_{1} n_{2}^{4} & - \sqrt{2} n_{1} n_{2} \cdot \left(2 \Delta\eta n_{0}^{2} + 2 \Delta\eta n_{1}^{2} + \mu_{0} n_{0}^{2} + \mu_{0} n_{1}^{2} - \mu_{1} n_{2}^{2}\right) & - \sqrt{2} n_{0} n_{2} \cdot \left(2 \Delta\eta n_{0}^{2} + 2 \Delta\eta n_{1}^{2} + \mu_{0} n_{0}^{2} + \mu_{0} n_{1}^{2} - \mu_{1} n_{2}^{2}\right) & \sqrt{2} n_{0} n_{1} \cdot \left(2 \Delta\eta n_{2}^{2} + \mu_{0} n_{2}^{2} - \mu_{0} + \mu_{1} n_{2}^{2}\right)\\\frac{\sqrt{2} n_{1} n_{2} \cdot \left(2 \Delta\eta n_{0}^{2} n_{2}^{2} - 2 \Delta\eta n_{0}^{2} + \mu_{0} n_{0}^{2} n_{2}^{2} + \mu_{0} n_{1}^{2} + \mu_{1} n_{0}^{2} n_{2}^{2} - \mu_{1} n_{0}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & \frac{\sqrt{2} n_{1} n_{2} \cdot \left(2 \Delta\eta n_{0}^{2} + 2 \Delta\eta n_{1}^{2} n_{2}^{2} + \mu_{0} n_{0}^{2} + \mu_{0} n_{1}^{2} n_{2}^{2} + \mu_{1} n_{1}^{2} n_{2}^{2} - \mu_{1} n_{1}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & - \sqrt{2} n_{1} n_{2} \cdot \left(2 \Delta\eta n_{0}^{2} + 2 \Delta\eta n_{1}^{2} + \mu_{0} n_{0}^{2} + \mu_{0} n_{1}^{2} - \mu_{1} n_{2}^{2}\right) & \frac{2 \left(\Delta\eta n_{0}^{2} n_{2}^{2} + 2 \Delta\eta n_{1}^{2} n_{2}^{4} - 2 \Delta\eta n_{1}^{2} n_{2}^{2} + \Delta\eta n_{1}^{2} + \eta_{0} n_{2}^{2} - \eta_{0} + \mu_{0} n_{1}^{2} n_{2}^{4} - \mu_{0} n_{1}^{2} n_{2}^{2} + \mu_{1} n_{1}^{2} n_{2}^{4} - \mu_{1} n_{1}^{2} n_{2}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & 2 n_{0} n_{1} \cdot \left(2 \Delta\eta n_{2}^{2} - \Delta\eta + \mu_{0} n_{2}^{2} + \mu_{1} n_{2}^{2}\right) & \frac{2 n_{0} n_{2} \left(\Delta\eta n_{0}^{2} + 2 \Delta\eta n_{1}^{2} n_{2}^{2} - \Delta\eta n_{1}^{2} + \mu_{0} n_{1}^{2} n_{2}^{2} - \mu_{0} n_{1}^{2} + \mu_{1} n_{1}^{2} n_{2}^{2} - \mu_{1} n_{1}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)}\\\frac{\sqrt{2} n_{0} n_{2} \cdot \left(2 \Delta\eta n_{0}^{2} n_{2}^{2} + 2 \Delta\eta n_{1}^{2} + \mu_{0} n_{0}^{2} n_{2}^{2} + \mu_{0} n_{1}^{2} + \mu_{1} n_{0}^{2} n_{2}^{2} - \mu_{1} n_{0}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & \frac{\sqrt{2} n_{0} n_{2} \cdot \left(2 \Delta\eta n_{1}^{2} n_{2}^{2} - 2 \Delta\eta n_{1}^{2} + \mu_{0} n_{0}^{2} + \mu_{0} n_{1}^{2} n_{2}^{2} + \mu_{1} n_{1}^{2} n_{2}^{2} - \mu_{1} n_{1}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & - \sqrt{2} n_{0} n_{2} \cdot \left(2 \Delta\eta n_{0}^{2} + 2 \Delta\eta n_{1}^{2} + \mu_{0} n_{0}^{2} + \mu_{0} n_{1}^{2} - \mu_{1} n_{2}^{2}\right) & 2 n_{0} n_{1} \cdot \left(2 \Delta\eta n_{2}^{2} - \Delta\eta + \mu_{0} n_{2}^{2} + \mu_{1} n_{2}^{2}\right) & \frac{2 \cdot \left(2 \Delta\eta n_{0}^{2} n_{2}^{4} - 2 \Delta\eta n_{0}^{2} n_{2}^{2} + \Delta\eta n_{0}^{2} + \Delta\eta n_{1}^{2} n_{2}^{2} + \eta_{0} n_{2}^{2} - \eta_{0} + \mu_{0} n_{0}^{2} n_{2}^{4} - \mu_{0} n_{0}^{2} n_{2}^{2} + \mu_{1} n_{0}^{2} n_{2}^{4} - \mu_{1} n_{0}^{2} n_{2}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & \frac{2 n_{1} n_{2} \cdot \left(2 \Delta\eta n_{0}^{2} n_{2}^{2} - \Delta\eta n_{0}^{2} + \Delta\eta n_{1}^{2} + \mu_{0} n_{0}^{2} n_{2}^{2} - \mu_{0} n_{0}^{2} + \mu_{1} n_{0}^{2} n_{2}^{2} - \mu_{1} n_{0}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)}\\\frac{\sqrt{2} n_{0} n_{1} \cdot \left(2 \Delta\eta n_{0}^{2} n_{2}^{2} + 2 \Delta\eta n_{1}^{2} + \mu_{0} n_{0}^{2} n_{2}^{2} + \mu_{0} n_{1}^{2} + \mu_{1} n_{0}^{2} n_{2}^{2} - \mu_{1} n_{0}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & \frac{\sqrt{2} n_{0} n_{1} \cdot \left(2 \Delta\eta n_{0}^{2} + 2 \Delta\eta n_{1}^{2} n_{2}^{2} + \mu_{0} n_{0}^{2} + \mu_{0} n_{1}^{2} n_{2}^{2} + \mu_{1} n_{1}^{2} n_{2}^{2} - \mu_{1} n_{1}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & \sqrt{2} n_{0} n_{1} \cdot \left(2 \Delta\eta n_{2}^{2} + \mu_{0} n_{2}^{2} - \mu_{0} + \mu_{1} n_{2}^{2}\right) & \frac{2 n_{0} n_{2} \left(\Delta\eta n_{0}^{2} + 2 \Delta\eta n_{1}^{2} n_{2}^{2} - \Delta\eta n_{1}^{2} + \mu_{0} n_{1}^{2} n_{2}^{2} - \mu_{0} n_{1}^{2} - \mu_{1} n_{0}^{2} n_{1}^{2} - \mu_{1} n_{1}^{4}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & \frac{2 n_{1} n_{2} \cdot \left(2 \Delta\eta n_{0}^{2} n_{2}^{2} - \Delta\eta n_{0}^{2} + \Delta\eta n_{1}^{2} + \mu_{0} n_{0}^{2} n_{2}^{2} - \mu_{0} n_{0}^{2} - \mu_{1} n_{0}^{4} - \mu_{1} n_{0}^{2} n_{1}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)} & \frac{2 \left(\Delta\eta n_{0}^{4} + 2 \Delta\eta n_{0}^{2} n_{1}^{2} n_{2}^{2} + \Delta\eta n_{1}^{4} + \eta_{0} n_{2}^{2} - \eta_{0} + \mu_{0} n_{0}^{2} n_{1}^{2} n_{2}^{2} - \mu_{0} n_{0}^{2} n_{1}^{2} + \mu_{1} n_{0}^{2} n_{1}^{2} n_{2}^{2} - \mu_{1} n_{0}^{2} n_{1}^{2}\right)}{\left(n_{2} - 1\right) \left(n_{2} + 1\right)}\end{matrix}\right]$$ +# +# The `underworld` / `sympy` implementation follows: +# + +# + +## The full incompressible, trans-iso model (4 independent unknowns) from Han and Wahr paper + +# Extra viscosity terms +mu0 = sympy.symbols(r"\mu_0") +mu1 = sympy.symbols(r"\mu_1") + +I = uw.maths.tensor.rank4_identity(3) * 0 +C_IJm_HW = uw.maths.tensor.rank4_to_mandel(I, 3) +C_IJm_HW[0,0] = mu0 + 2 * eta0 +C_IJm_HW[0,1] = mu0 +C_IJm_HW[1,0] = mu0 +C_IJm_HW[1,1] = mu0 + 2 * eta0 +C_IJm_HW[2,2] = mu1 + 2 * (eta0 - delta_eta) +C_IJm_HW[3,3] = 2 * (eta0 - delta_eta) # yz +C_IJm_HW[4,4] = 2 * (eta0 - delta_eta) # xz +C_IJm_HW[5,5] = 2 * eta0 # xy + +display(C_IJm_HW) +# - + +# ## Rotation (symbolic) +# +# Compute the rotation (of the non-isotropic part) + +# + +C_ijkl_HW = uw.maths.tensor.mandel_to_rank4(C_IJm_HW - C_IJm, 3) +display(uw.maths.tensor.rank4_to_mandel(C_ijkl_HW, 3)) + +C_ijkl_HW_R = sympy.simplify(uw.maths.tensor.tensor_rotation(R, C_ijkl_HW)) +C_IJm_HW_R = sympy.simplify(uw.maths.tensor.rank4_to_mandel(C_ijkl_HW_R, 3)) + C_IJm + +display(C_IJm_HW_R) + +# + +## Maybe this can be simplified if we use the unit vector relationships among n0,n1,n2 + +C_IJm_HW_R_s1 = C_IJm_HW_R.subs(n[0]**2+n[1]**2+n[2]**2,1) +C_IJm_HW_R_s2 = C_IJm_HW_R_s1.subs(n[0]**2+n[1]**2, 1-n[2]**2).applyfunc(sympy.factor) +display(C_IJm_HW_R_s2) + +## Yeah, maybe not ... +# - + +# ## Orthotropic medium +# +# **Note** all the caveats above regarding incompressibility. The Browaeys & Chevrot elastic tensors have a bulk modulus term, so it is not completely obvious how to square the assumptions in the first two implementations with this set. +# +# The full formulation should look like this: +# +# $$\left[\begin{matrix}2 \eta_{00} & 2 \eta_{01} & 2 \eta_{02} & 0 & 0 & 0\\2 \eta_{01} & 2 \eta_{11} & 2 \eta_{12} & 0 & 0 & 0\\2 \eta_{02} & 2 \eta_{12} & 2 \eta_{22} & 0 & 0 & 0\\0 & 0 & 0 & 2 \eta_{33} & 0 & 0\\0 & 0 & 0 & 0 & 2 \eta_{44} & 0\\0 & 0 & 0 & 0 & 0 & 2 \eta_{55}\end{matrix}\right]$$ +# +# Rotation in this case should be general as it is no longer enough to specify the symmetry plane. +# +# $$\left[\begin{matrix}s_{0} & t_{0} & n_{0}\\s_{1} & t_{1} & n_{1}\\s_{2} & t_{2} & n_{2}\end{matrix}\right]$$ +# +# $\hat{\mathbf{n}}$, $\hat{\mathbf{s}}$ and $\hat{\mathbf{t}}$ are an arbitrary orthogonal triad of unit vectors (we keep the notation from the Mühlhaus formulation). It is probably not useful to code up this form. +# +# $$\left[\begin{matrix}2 \eta_{00} s_{0}^{4} + 4 \eta_{01} s_{0}^{2} t_{0}^{2} + 4 \eta_{02} n_{0}^{2} s_{0}^{2} + 2 \eta_{11} t_{0}^{4} + 4 \eta_{12} n_{0}^{2} t_{0}^{2} + 2 \eta_{22} n_{0}^{4} + 4 \eta_{33} n_{0}^{2} t_{0}^{2} + 4 \eta_{44} n_{0}^{2} s_{0}^{2} + 4 \eta_{55} s_{0}^{2} t_{0}^{2} & 2 n_{0} \left(\eta_{33} n_{1} t_{0} t_{1} + \eta_{44} n_{1} s_{0} s_{1} + n_{0} \left(\eta_{02} s_{1}^{2} + \eta_{12} t_{1}^{2} + \eta_{22} n_{1}^{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{0} n_{1} s_{1} + \eta_{55} s_{1} t_{0} t_{1} + s_{0} \left(\eta_{00} s_{1}^{2} + \eta_{01} t_{1}^{2} + \eta_{02} n_{1}^{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{0} n_{1} t_{1} + \eta_{55} s_{0} s_{1} t_{1} + t_{0} \left(\eta_{01} s_{1}^{2} + \eta_{11} t_{1}^{2} + \eta_{12} n_{1}^{2}\right)\right) & 2 n_{0} \left(\eta_{33} n_{2} t_{0} t_{2} + \eta_{44} n_{2} s_{0} s_{2} + n_{0} \left(\eta_{02} s_{2}^{2} + \eta_{12} t_{2}^{2} + \eta_{22} n_{2}^{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{0} n_{2} s_{2} + \eta_{55} s_{2} t_{0} t_{2} + s_{0} \left(\eta_{00} s_{2}^{2} + \eta_{01} t_{2}^{2} + \eta_{02} n_{2}^{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{0} n_{2} t_{2} + \eta_{55} s_{0} s_{2} t_{2} + t_{0} \left(\eta_{01} s_{2}^{2} + \eta_{11} t_{2}^{2} + \eta_{12} n_{2}^{2}\right)\right) & \sqrt{2} \left(n_{0} \left(\eta_{33} t_{0} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{44} s_{0} \left(n_{1} s_{2} + n_{2} s_{1}\right) + 2 n_{0} \left(\eta_{02} s_{1} s_{2} + \eta_{12} t_{1} t_{2} + \eta_{22} n_{1} n_{2}\right)\right) + s_{0} \left(\eta_{44} n_{0} \left(n_{1} s_{2} + n_{2} s_{1}\right) + \eta_{55} t_{0} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 s_{0} \left(\eta_{00} s_{1} s_{2} + \eta_{01} t_{1} t_{2} + \eta_{02} n_{1} n_{2}\right)\right) + t_{0} \left(\eta_{33} n_{0} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{55} s_{0} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 t_{0} \left(\eta_{01} s_{1} s_{2} + \eta_{11} t_{1} t_{2} + \eta_{12} n_{1} n_{2}\right)\right)\right) & \sqrt{2} \left(n_{0} \left(\eta_{33} t_{0} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{44} s_{0} \left(n_{0} s_{2} + n_{2} s_{0}\right) + 2 n_{0} \left(\eta_{02} s_{0} s_{2} + \eta_{12} t_{0} t_{2} + \eta_{22} n_{0} n_{2}\right)\right) + s_{0} \left(\eta_{44} n_{0} \left(n_{0} s_{2} + n_{2} s_{0}\right) + \eta_{55} t_{0} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 s_{0} \left(\eta_{00} s_{0} s_{2} + \eta_{01} t_{0} t_{2} + \eta_{02} n_{0} n_{2}\right)\right) + t_{0} \left(\eta_{33} n_{0} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{55} s_{0} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 t_{0} \left(\eta_{01} s_{0} s_{2} + \eta_{11} t_{0} t_{2} + \eta_{12} n_{0} n_{2}\right)\right)\right) & \sqrt{2} \left(n_{0} \left(\eta_{33} t_{0} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{44} s_{0} \left(n_{0} s_{1} + n_{1} s_{0}\right) + 2 n_{0} \left(\eta_{02} s_{0} s_{1} + \eta_{12} t_{0} t_{1} + \eta_{22} n_{0} n_{1}\right)\right) + s_{0} \left(\eta_{44} n_{0} \left(n_{0} s_{1} + n_{1} s_{0}\right) + \eta_{55} t_{0} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 s_{0} \left(\eta_{00} s_{0} s_{1} + \eta_{01} t_{0} t_{1} + \eta_{02} n_{0} n_{1}\right)\right) + t_{0} \left(\eta_{33} n_{0} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{55} s_{0} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 t_{0} \left(\eta_{01} s_{0} s_{1} + \eta_{11} t_{0} t_{1} + \eta_{12} n_{0} n_{1}\right)\right)\right)\\2 n_{1} \left(\eta_{33} n_{0} t_{0} t_{1} + \eta_{44} n_{0} s_{0} s_{1} + n_{1} \left(\eta_{02} s_{0}^{2} + \eta_{12} t_{0}^{2} + \eta_{22} n_{0}^{2}\right)\right) + 2 s_{1} \left(\eta_{44} n_{0} n_{1} s_{0} + \eta_{55} s_{0} t_{0} t_{1} + s_{1} \left(\eta_{00} s_{0}^{2} + \eta_{01} t_{0}^{2} + \eta_{02} n_{0}^{2}\right)\right) + 2 t_{1} \left(\eta_{33} n_{0} n_{1} t_{0} + \eta_{55} s_{0} s_{1} t_{0} + t_{1} \left(\eta_{01} s_{0}^{2} + \eta_{11} t_{0}^{2} + \eta_{12} n_{0}^{2}\right)\right) & 2 \eta_{00} s_{1}^{4} + 4 \eta_{01} s_{1}^{2} t_{1}^{2} + 4 \eta_{02} n_{1}^{2} s_{1}^{2} + 2 \eta_{11} t_{1}^{4} + 4 \eta_{12} n_{1}^{2} t_{1}^{2} + 2 \eta_{22} n_{1}^{4} + 4 \eta_{33} n_{1}^{2} t_{1}^{2} + 4 \eta_{44} n_{1}^{2} s_{1}^{2} + 4 \eta_{55} s_{1}^{2} t_{1}^{2} & 2 n_{1} \left(\eta_{33} n_{2} t_{1} t_{2} + \eta_{44} n_{2} s_{1} s_{2} + n_{1} \left(\eta_{02} s_{2}^{2} + \eta_{12} t_{2}^{2} + \eta_{22} n_{2}^{2}\right)\right) + 2 s_{1} \left(\eta_{44} n_{1} n_{2} s_{2} + \eta_{55} s_{2} t_{1} t_{2} + s_{1} \left(\eta_{00} s_{2}^{2} + \eta_{01} t_{2}^{2} + \eta_{02} n_{2}^{2}\right)\right) + 2 t_{1} \left(\eta_{33} n_{1} n_{2} t_{2} + \eta_{55} s_{1} s_{2} t_{2} + t_{1} \left(\eta_{01} s_{2}^{2} + \eta_{11} t_{2}^{2} + \eta_{12} n_{2}^{2}\right)\right) & \sqrt{2} \left(n_{1} \left(\eta_{33} t_{1} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{44} s_{1} \left(n_{1} s_{2} + n_{2} s_{1}\right) + 2 n_{1} \left(\eta_{02} s_{1} s_{2} + \eta_{12} t_{1} t_{2} + \eta_{22} n_{1} n_{2}\right)\right) + s_{1} \left(\eta_{44} n_{1} \left(n_{1} s_{2} + n_{2} s_{1}\right) + \eta_{55} t_{1} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 s_{1} \left(\eta_{00} s_{1} s_{2} + \eta_{01} t_{1} t_{2} + \eta_{02} n_{1} n_{2}\right)\right) + t_{1} \left(\eta_{33} n_{1} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{55} s_{1} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 t_{1} \left(\eta_{01} s_{1} s_{2} + \eta_{11} t_{1} t_{2} + \eta_{12} n_{1} n_{2}\right)\right)\right) & \sqrt{2} \left(n_{1} \left(\eta_{33} t_{1} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{44} s_{1} \left(n_{0} s_{2} + n_{2} s_{0}\right) + 2 n_{1} \left(\eta_{02} s_{0} s_{2} + \eta_{12} t_{0} t_{2} + \eta_{22} n_{0} n_{2}\right)\right) + s_{1} \left(\eta_{44} n_{1} \left(n_{0} s_{2} + n_{2} s_{0}\right) + \eta_{55} t_{1} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 s_{1} \left(\eta_{00} s_{0} s_{2} + \eta_{01} t_{0} t_{2} + \eta_{02} n_{0} n_{2}\right)\right) + t_{1} \left(\eta_{33} n_{1} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{55} s_{1} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 t_{1} \left(\eta_{01} s_{0} s_{2} + \eta_{11} t_{0} t_{2} + \eta_{12} n_{0} n_{2}\right)\right)\right) & \sqrt{2} \left(n_{1} \left(\eta_{33} t_{1} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{44} s_{1} \left(n_{0} s_{1} + n_{1} s_{0}\right) + 2 n_{1} \left(\eta_{02} s_{0} s_{1} + \eta_{12} t_{0} t_{1} + \eta_{22} n_{0} n_{1}\right)\right) + s_{1} \left(\eta_{44} n_{1} \left(n_{0} s_{1} + n_{1} s_{0}\right) + \eta_{55} t_{1} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 s_{1} \left(\eta_{00} s_{0} s_{1} + \eta_{01} t_{0} t_{1} + \eta_{02} n_{0} n_{1}\right)\right) + t_{1} \left(\eta_{33} n_{1} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{55} s_{1} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 t_{1} \left(\eta_{01} s_{0} s_{1} + \eta_{11} t_{0} t_{1} + \eta_{12} n_{0} n_{1}\right)\right)\right)\\2 n_{2} \left(\eta_{33} n_{0} t_{0} t_{2} + \eta_{44} n_{0} s_{0} s_{2} + n_{2} \left(\eta_{02} s_{0}^{2} + \eta_{12} t_{0}^{2} + \eta_{22} n_{0}^{2}\right)\right) + 2 s_{2} \left(\eta_{44} n_{0} n_{2} s_{0} + \eta_{55} s_{0} t_{0} t_{2} + s_{2} \left(\eta_{00} s_{0}^{2} + \eta_{01} t_{0}^{2} + \eta_{02} n_{0}^{2}\right)\right) + 2 t_{2} \left(\eta_{33} n_{0} n_{2} t_{0} + \eta_{55} s_{0} s_{2} t_{0} + t_{2} \left(\eta_{01} s_{0}^{2} + \eta_{11} t_{0}^{2} + \eta_{12} n_{0}^{2}\right)\right) & 2 n_{2} \left(\eta_{33} n_{1} t_{1} t_{2} + \eta_{44} n_{1} s_{1} s_{2} + n_{2} \left(\eta_{02} s_{1}^{2} + \eta_{12} t_{1}^{2} + \eta_{22} n_{1}^{2}\right)\right) + 2 s_{2} \left(\eta_{44} n_{1} n_{2} s_{1} + \eta_{55} s_{1} t_{1} t_{2} + s_{2} \left(\eta_{00} s_{1}^{2} + \eta_{01} t_{1}^{2} + \eta_{02} n_{1}^{2}\right)\right) + 2 t_{2} \left(\eta_{33} n_{1} n_{2} t_{1} + \eta_{55} s_{1} s_{2} t_{1} + t_{2} \left(\eta_{01} s_{1}^{2} + \eta_{11} t_{1}^{2} + \eta_{12} n_{1}^{2}\right)\right) & 2 \eta_{00} s_{2}^{4} + 4 \eta_{01} s_{2}^{2} t_{2}^{2} + 4 \eta_{02} n_{2}^{2} s_{2}^{2} + 2 \eta_{11} t_{2}^{4} + 4 \eta_{12} n_{2}^{2} t_{2}^{2} + 2 \eta_{22} n_{2}^{4} + 4 \eta_{33} n_{2}^{2} t_{2}^{2} + 4 \eta_{44} n_{2}^{2} s_{2}^{2} + 4 \eta_{55} s_{2}^{2} t_{2}^{2} & \sqrt{2} \left(n_{2} \left(\eta_{33} t_{2} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{44} s_{2} \left(n_{1} s_{2} + n_{2} s_{1}\right) + 2 n_{2} \left(\eta_{02} s_{1} s_{2} + \eta_{12} t_{1} t_{2} + \eta_{22} n_{1} n_{2}\right)\right) + s_{2} \left(\eta_{44} n_{2} \left(n_{1} s_{2} + n_{2} s_{1}\right) + \eta_{55} t_{2} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 s_{2} \left(\eta_{00} s_{1} s_{2} + \eta_{01} t_{1} t_{2} + \eta_{02} n_{1} n_{2}\right)\right) + t_{2} \left(\eta_{33} n_{2} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{55} s_{2} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 t_{2} \left(\eta_{01} s_{1} s_{2} + \eta_{11} t_{1} t_{2} + \eta_{12} n_{1} n_{2}\right)\right)\right) & \sqrt{2} \left(n_{2} \left(\eta_{33} t_{2} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{44} s_{2} \left(n_{0} s_{2} + n_{2} s_{0}\right) + 2 n_{2} \left(\eta_{02} s_{0} s_{2} + \eta_{12} t_{0} t_{2} + \eta_{22} n_{0} n_{2}\right)\right) + s_{2} \left(\eta_{44} n_{2} \left(n_{0} s_{2} + n_{2} s_{0}\right) + \eta_{55} t_{2} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 s_{2} \left(\eta_{00} s_{0} s_{2} + \eta_{01} t_{0} t_{2} + \eta_{02} n_{0} n_{2}\right)\right) + t_{2} \left(\eta_{33} n_{2} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{55} s_{2} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 t_{2} \left(\eta_{01} s_{0} s_{2} + \eta_{11} t_{0} t_{2} + \eta_{12} n_{0} n_{2}\right)\right)\right) & \sqrt{2} \left(n_{2} \left(\eta_{33} t_{2} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{44} s_{2} \left(n_{0} s_{1} + n_{1} s_{0}\right) + 2 n_{2} \left(\eta_{02} s_{0} s_{1} + \eta_{12} t_{0} t_{1} + \eta_{22} n_{0} n_{1}\right)\right) + s_{2} \left(\eta_{44} n_{2} \left(n_{0} s_{1} + n_{1} s_{0}\right) + \eta_{55} t_{2} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 s_{2} \left(\eta_{00} s_{0} s_{1} + \eta_{01} t_{0} t_{1} + \eta_{02} n_{0} n_{1}\right)\right) + t_{2} \left(\eta_{33} n_{2} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{55} s_{2} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 t_{2} \left(\eta_{01} s_{0} s_{1} + \eta_{11} t_{0} t_{1} + \eta_{12} n_{0} n_{1}\right)\right)\right)\\\sqrt{2} \cdot \left(2 n_{1} \left(\eta_{33} n_{0} t_{0} t_{2} + \eta_{44} n_{0} s_{0} s_{2} + n_{2} \left(\eta_{02} s_{0}^{2} + \eta_{12} t_{0}^{2} + \eta_{22} n_{0}^{2}\right)\right) + 2 s_{1} \left(\eta_{44} n_{0} n_{2} s_{0} + \eta_{55} s_{0} t_{0} t_{2} + s_{2} \left(\eta_{00} s_{0}^{2} + \eta_{01} t_{0}^{2} + \eta_{02} n_{0}^{2}\right)\right) + 2 t_{1} \left(\eta_{33} n_{0} n_{2} t_{0} + \eta_{55} s_{0} s_{2} t_{0} + t_{2} \left(\eta_{01} s_{0}^{2} + \eta_{11} t_{0}^{2} + \eta_{12} n_{0}^{2}\right)\right)\right) & \sqrt{2} \cdot \left(2 n_{1} \left(\eta_{33} n_{1} t_{1} t_{2} + \eta_{44} n_{1} s_{1} s_{2} + n_{2} \left(\eta_{02} s_{1}^{2} + \eta_{12} t_{1}^{2} + \eta_{22} n_{1}^{2}\right)\right) + 2 s_{1} \left(\eta_{44} n_{1} n_{2} s_{1} + \eta_{55} s_{1} t_{1} t_{2} + s_{2} \left(\eta_{00} s_{1}^{2} + \eta_{01} t_{1}^{2} + \eta_{02} n_{1}^{2}\right)\right) + 2 t_{1} \left(\eta_{33} n_{1} n_{2} t_{1} + \eta_{55} s_{1} s_{2} t_{1} + t_{2} \left(\eta_{01} s_{1}^{2} + \eta_{11} t_{1}^{2} + \eta_{12} n_{1}^{2}\right)\right)\right) & \sqrt{2} \cdot \left(2 n_{1} n_{2} \left(\eta_{02} s_{2}^{2} + \eta_{12} t_{2}^{2} + \eta_{22} n_{2}^{2} + \eta_{33} t_{2}^{2} + \eta_{44} s_{2}^{2}\right) + 2 s_{1} s_{2} \left(\eta_{00} s_{2}^{2} + \eta_{01} t_{2}^{2} + \eta_{02} n_{2}^{2} + \eta_{44} n_{2}^{2} + \eta_{55} t_{2}^{2}\right) + 2 t_{1} t_{2} \left(\eta_{01} s_{2}^{2} + \eta_{11} t_{2}^{2} + \eta_{12} n_{2}^{2} + \eta_{33} n_{2}^{2} + \eta_{55} s_{2}^{2}\right)\right) & 2 n_{1} \left(\eta_{33} t_{2} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{44} s_{2} \left(n_{1} s_{2} + n_{2} s_{1}\right) + 2 n_{2} \left(\eta_{02} s_{1} s_{2} + \eta_{12} t_{1} t_{2} + \eta_{22} n_{1} n_{2}\right)\right) + 2 s_{1} \left(\eta_{44} n_{2} \left(n_{1} s_{2} + n_{2} s_{1}\right) + \eta_{55} t_{2} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 s_{2} \left(\eta_{00} s_{1} s_{2} + \eta_{01} t_{1} t_{2} + \eta_{02} n_{1} n_{2}\right)\right) + 2 t_{1} \left(\eta_{33} n_{2} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{55} s_{2} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 t_{2} \left(\eta_{01} s_{1} s_{2} + \eta_{11} t_{1} t_{2} + \eta_{12} n_{1} n_{2}\right)\right) & 2 n_{1} \left(\eta_{33} t_{2} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{44} s_{2} \left(n_{0} s_{2} + n_{2} s_{0}\right) + 2 n_{2} \left(\eta_{02} s_{0} s_{2} + \eta_{12} t_{0} t_{2} + \eta_{22} n_{0} n_{2}\right)\right) + 2 s_{1} \left(\eta_{44} n_{2} \left(n_{0} s_{2} + n_{2} s_{0}\right) + \eta_{55} t_{2} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 s_{2} \left(\eta_{00} s_{0} s_{2} + \eta_{01} t_{0} t_{2} + \eta_{02} n_{0} n_{2}\right)\right) + 2 t_{1} \left(\eta_{33} n_{2} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{55} s_{2} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 t_{2} \left(\eta_{01} s_{0} s_{2} + \eta_{11} t_{0} t_{2} + \eta_{12} n_{0} n_{2}\right)\right) & 2 n_{1} \left(\eta_{33} t_{2} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{44} s_{2} \left(n_{0} s_{1} + n_{1} s_{0}\right) + 2 n_{2} \left(\eta_{02} s_{0} s_{1} + \eta_{12} t_{0} t_{1} + \eta_{22} n_{0} n_{1}\right)\right) + 2 s_{1} \left(\eta_{44} n_{2} \left(n_{0} s_{1} + n_{1} s_{0}\right) + \eta_{55} t_{2} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 s_{2} \left(\eta_{00} s_{0} s_{1} + \eta_{01} t_{0} t_{1} + \eta_{02} n_{0} n_{1}\right)\right) + 2 t_{1} \left(\eta_{33} n_{2} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{55} s_{2} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 t_{2} \left(\eta_{01} s_{0} s_{1} + \eta_{11} t_{0} t_{1} + \eta_{12} n_{0} n_{1}\right)\right)\\\sqrt{2} \cdot \left(2 n_{0} \left(\eta_{33} n_{0} t_{0} t_{2} + \eta_{44} n_{0} s_{0} s_{2} + n_{2} \left(\eta_{02} s_{0}^{2} + \eta_{12} t_{0}^{2} + \eta_{22} n_{0}^{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{0} n_{2} s_{0} + \eta_{55} s_{0} t_{0} t_{2} + s_{2} \left(\eta_{00} s_{0}^{2} + \eta_{01} t_{0}^{2} + \eta_{02} n_{0}^{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{0} n_{2} t_{0} + \eta_{55} s_{0} s_{2} t_{0} + t_{2} \left(\eta_{01} s_{0}^{2} + \eta_{11} t_{0}^{2} + \eta_{12} n_{0}^{2}\right)\right)\right) & \sqrt{2} \cdot \left(2 n_{0} \left(\eta_{33} n_{1} t_{1} t_{2} + \eta_{44} n_{1} s_{1} s_{2} + n_{2} \left(\eta_{02} s_{1}^{2} + \eta_{12} t_{1}^{2} + \eta_{22} n_{1}^{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{1} n_{2} s_{1} + \eta_{55} s_{1} t_{1} t_{2} + s_{2} \left(\eta_{00} s_{1}^{2} + \eta_{01} t_{1}^{2} + \eta_{02} n_{1}^{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{1} n_{2} t_{1} + \eta_{55} s_{1} s_{2} t_{1} + t_{2} \left(\eta_{01} s_{1}^{2} + \eta_{11} t_{1}^{2} + \eta_{12} n_{1}^{2}\right)\right)\right) & \sqrt{2} \cdot \left(2 n_{0} n_{2} \left(\eta_{02} s_{2}^{2} + \eta_{12} t_{2}^{2} + \eta_{22} n_{2}^{2} + \eta_{33} t_{2}^{2} + \eta_{44} s_{2}^{2}\right) + 2 s_{0} s_{2} \left(\eta_{00} s_{2}^{2} + \eta_{01} t_{2}^{2} + \eta_{02} n_{2}^{2} + \eta_{44} n_{2}^{2} + \eta_{55} t_{2}^{2}\right) + 2 t_{0} t_{2} \left(\eta_{01} s_{2}^{2} + \eta_{11} t_{2}^{2} + \eta_{12} n_{2}^{2} + \eta_{33} n_{2}^{2} + \eta_{55} s_{2}^{2}\right)\right) & 2 n_{0} \left(\eta_{33} t_{2} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{44} s_{2} \left(n_{1} s_{2} + n_{2} s_{1}\right) + 2 n_{2} \left(\eta_{02} s_{1} s_{2} + \eta_{12} t_{1} t_{2} + \eta_{22} n_{1} n_{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{2} \left(n_{1} s_{2} + n_{2} s_{1}\right) + \eta_{55} t_{2} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 s_{2} \left(\eta_{00} s_{1} s_{2} + \eta_{01} t_{1} t_{2} + \eta_{02} n_{1} n_{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{2} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{55} s_{2} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 t_{2} \left(\eta_{01} s_{1} s_{2} + \eta_{11} t_{1} t_{2} + \eta_{12} n_{1} n_{2}\right)\right) & 2 n_{0} \left(\eta_{33} t_{2} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{44} s_{2} \left(n_{0} s_{2} + n_{2} s_{0}\right) + 2 n_{2} \left(\eta_{02} s_{0} s_{2} + \eta_{12} t_{0} t_{2} + \eta_{22} n_{0} n_{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{2} \left(n_{0} s_{2} + n_{2} s_{0}\right) + \eta_{55} t_{2} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 s_{2} \left(\eta_{00} s_{0} s_{2} + \eta_{01} t_{0} t_{2} + \eta_{02} n_{0} n_{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{2} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{55} s_{2} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 t_{2} \left(\eta_{01} s_{0} s_{2} + \eta_{11} t_{0} t_{2} + \eta_{12} n_{0} n_{2}\right)\right) & 2 n_{0} \left(\eta_{33} t_{2} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{44} s_{2} \left(n_{0} s_{1} + n_{1} s_{0}\right) + 2 n_{2} \left(\eta_{02} s_{0} s_{1} + \eta_{12} t_{0} t_{1} + \eta_{22} n_{0} n_{1}\right)\right) + 2 s_{0} \left(\eta_{44} n_{2} \left(n_{0} s_{1} + n_{1} s_{0}\right) + \eta_{55} t_{2} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 s_{2} \left(\eta_{00} s_{0} s_{1} + \eta_{01} t_{0} t_{1} + \eta_{02} n_{0} n_{1}\right)\right) + 2 t_{0} \left(\eta_{33} n_{2} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{55} s_{2} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 t_{2} \left(\eta_{01} s_{0} s_{1} + \eta_{11} t_{0} t_{1} + \eta_{12} n_{0} n_{1}\right)\right)\\\sqrt{2} \cdot \left(2 n_{0} \left(\eta_{33} n_{0} t_{0} t_{1} + \eta_{44} n_{0} s_{0} s_{1} + n_{1} \left(\eta_{02} s_{0}^{2} + \eta_{12} t_{0}^{2} + \eta_{22} n_{0}^{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{0} n_{1} s_{0} + \eta_{55} s_{0} t_{0} t_{1} + s_{1} \left(\eta_{00} s_{0}^{2} + \eta_{01} t_{0}^{2} + \eta_{02} n_{0}^{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{0} n_{1} t_{0} + \eta_{55} s_{0} s_{1} t_{0} + t_{1} \left(\eta_{01} s_{0}^{2} + \eta_{11} t_{0}^{2} + \eta_{12} n_{0}^{2}\right)\right)\right) & \sqrt{2} \cdot \left(2 n_{0} n_{1} \left(\eta_{02} s_{1}^{2} + \eta_{12} t_{1}^{2} + \eta_{22} n_{1}^{2} + \eta_{33} t_{1}^{2} + \eta_{44} s_{1}^{2}\right) + 2 s_{0} s_{1} \left(\eta_{00} s_{1}^{2} + \eta_{01} t_{1}^{2} + \eta_{02} n_{1}^{2} + \eta_{44} n_{1}^{2} + \eta_{55} t_{1}^{2}\right) + 2 t_{0} t_{1} \left(\eta_{01} s_{1}^{2} + \eta_{11} t_{1}^{2} + \eta_{12} n_{1}^{2} + \eta_{33} n_{1}^{2} + \eta_{55} s_{1}^{2}\right)\right) & \sqrt{2} \cdot \left(2 n_{0} \left(\eta_{33} n_{2} t_{1} t_{2} + \eta_{44} n_{2} s_{1} s_{2} + n_{1} \left(\eta_{02} s_{2}^{2} + \eta_{12} t_{2}^{2} + \eta_{22} n_{2}^{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{1} n_{2} s_{2} + \eta_{55} s_{2} t_{1} t_{2} + s_{1} \left(\eta_{00} s_{2}^{2} + \eta_{01} t_{2}^{2} + \eta_{02} n_{2}^{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{1} n_{2} t_{2} + \eta_{55} s_{1} s_{2} t_{2} + t_{1} \left(\eta_{01} s_{2}^{2} + \eta_{11} t_{2}^{2} + \eta_{12} n_{2}^{2}\right)\right)\right) & 2 n_{0} \left(\eta_{33} t_{1} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{44} s_{1} \left(n_{1} s_{2} + n_{2} s_{1}\right) + 2 n_{1} \left(\eta_{02} s_{1} s_{2} + \eta_{12} t_{1} t_{2} + \eta_{22} n_{1} n_{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{1} \left(n_{1} s_{2} + n_{2} s_{1}\right) + \eta_{55} t_{1} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 s_{1} \left(\eta_{00} s_{1} s_{2} + \eta_{01} t_{1} t_{2} + \eta_{02} n_{1} n_{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{1} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{55} s_{1} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 t_{1} \left(\eta_{01} s_{1} s_{2} + \eta_{11} t_{1} t_{2} + \eta_{12} n_{1} n_{2}\right)\right) & 2 n_{0} \left(\eta_{33} t_{1} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{44} s_{1} \left(n_{0} s_{2} + n_{2} s_{0}\right) + 2 n_{1} \left(\eta_{02} s_{0} s_{2} + \eta_{12} t_{0} t_{2} + \eta_{22} n_{0} n_{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{1} \left(n_{0} s_{2} + n_{2} s_{0}\right) + \eta_{55} t_{1} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 s_{1} \left(\eta_{00} s_{0} s_{2} + \eta_{01} t_{0} t_{2} + \eta_{02} n_{0} n_{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{1} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{55} s_{1} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 t_{1} \left(\eta_{01} s_{0} s_{2} + \eta_{11} t_{0} t_{2} + \eta_{12} n_{0} n_{2}\right)\right) & 2 n_{0} \left(\eta_{33} t_{1} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{44} s_{1} \left(n_{0} s_{1} + n_{1} s_{0}\right) + 2 n_{1} \left(\eta_{02} s_{0} s_{1} + \eta_{12} t_{0} t_{1} + \eta_{22} n_{0} n_{1}\right)\right) + 2 s_{0} \left(\eta_{44} n_{1} \left(n_{0} s_{1} + n_{1} s_{0}\right) + \eta_{55} t_{1} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 s_{1} \left(\eta_{00} s_{0} s_{1} + \eta_{01} t_{0} t_{1} + \eta_{02} n_{0} n_{1}\right)\right) + 2 t_{0} \left(\eta_{33} n_{1} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{55} s_{1} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 t_{1} \left(\eta_{01} s_{0} s_{1} + \eta_{11} t_{0} t_{1} + \eta_{12} n_{0} n_{1}\right)\right)\end{matrix}\right]$$ +# +# In the Browaeys formulation, the orthorhombic part has two unique values (in the Voigt notation this gives three unique entries in the $C_{IJ}$ matrix). The canonical (Mandel) form is +# +# $$\left[\begin{matrix}2 \xi_{0} & 0 & 2 \xi_{1} & 0 & 0 & 0\\0 & - 2 \xi_{0} & - 2 \xi_{1} & 0 & 0 & 0\\2 \xi_{1} & - 2 \xi_{1} & 0 & 0 & 0 & 0\\0 & 0 & 0 & - 2 \xi_{1} & 0 & 0\\0 & 0 & 0 & 0 & 2 \xi_{1} & 0\\0 & 0 & 0 & 0 & 0 & 0\end{matrix}\right]$$ +# +# The rotated form: +# +# $$\left[\begin{matrix}2 \eta_{00} s_{0}^{4} + 4 \eta_{01} s_{0}^{2} t_{0}^{2} + 4 \eta_{02} n_{0}^{2} s_{0}^{2} + 2 \eta_{11} t_{0}^{4} + 4 \eta_{12} n_{0}^{2} t_{0}^{2} + 2 \eta_{22} n_{0}^{4} + 4 \eta_{33} n_{0}^{2} t_{0}^{2} + 4 \eta_{44} n_{0}^{2} s_{0}^{2} + 4 \eta_{55} s_{0}^{2} t_{0}^{2} & 2 n_{0} \left(\eta_{33} n_{1} t_{0} t_{1} + \eta_{44} n_{1} s_{0} s_{1} + n_{0} \left(\eta_{02} s_{1}^{2} + \eta_{12} t_{1}^{2} + \eta_{22} n_{1}^{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{0} n_{1} s_{1} + \eta_{55} s_{1} t_{0} t_{1} + s_{0} \left(\eta_{00} s_{1}^{2} + \eta_{01} t_{1}^{2} + \eta_{02} n_{1}^{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{0} n_{1} t_{1} + \eta_{55} s_{0} s_{1} t_{1} + t_{0} \left(\eta_{01} s_{1}^{2} + \eta_{11} t_{1}^{2} + \eta_{12} n_{1}^{2}\right)\right) & 2 n_{0} \left(\eta_{33} n_{2} t_{0} t_{2} + \eta_{44} n_{2} s_{0} s_{2} + n_{0} \left(\eta_{02} s_{2}^{2} + \eta_{12} t_{2}^{2} + \eta_{22} n_{2}^{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{0} n_{2} s_{2} + \eta_{55} s_{2} t_{0} t_{2} + s_{0} \left(\eta_{00} s_{2}^{2} + \eta_{01} t_{2}^{2} + \eta_{02} n_{2}^{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{0} n_{2} t_{2} + \eta_{55} s_{0} s_{2} t_{2} + t_{0} \left(\eta_{01} s_{2}^{2} + \eta_{11} t_{2}^{2} + \eta_{12} n_{2}^{2}\right)\right) & \sqrt{2} \left(n_{0} \left(\eta_{33} t_{0} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{44} s_{0} \left(n_{1} s_{2} + n_{2} s_{1}\right) + 2 n_{0} \left(\eta_{02} s_{1} s_{2} + \eta_{12} t_{1} t_{2} + \eta_{22} n_{1} n_{2}\right)\right) + s_{0} \left(\eta_{44} n_{0} \left(n_{1} s_{2} + n_{2} s_{1}\right) + \eta_{55} t_{0} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 s_{0} \left(\eta_{00} s_{1} s_{2} + \eta_{01} t_{1} t_{2} + \eta_{02} n_{1} n_{2}\right)\right) + t_{0} \left(\eta_{33} n_{0} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{55} s_{0} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 t_{0} \left(\eta_{01} s_{1} s_{2} + \eta_{11} t_{1} t_{2} + \eta_{12} n_{1} n_{2}\right)\right)\right) & \sqrt{2} \left(n_{0} \left(\eta_{33} t_{0} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{44} s_{0} \left(n_{0} s_{2} + n_{2} s_{0}\right) + 2 n_{0} \left(\eta_{02} s_{0} s_{2} + \eta_{12} t_{0} t_{2} + \eta_{22} n_{0} n_{2}\right)\right) + s_{0} \left(\eta_{44} n_{0} \left(n_{0} s_{2} + n_{2} s_{0}\right) + \eta_{55} t_{0} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 s_{0} \left(\eta_{00} s_{0} s_{2} + \eta_{01} t_{0} t_{2} + \eta_{02} n_{0} n_{2}\right)\right) + t_{0} \left(\eta_{33} n_{0} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{55} s_{0} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 t_{0} \left(\eta_{01} s_{0} s_{2} + \eta_{11} t_{0} t_{2} + \eta_{12} n_{0} n_{2}\right)\right)\right) & \sqrt{2} \left(n_{0} \left(\eta_{33} t_{0} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{44} s_{0} \left(n_{0} s_{1} + n_{1} s_{0}\right) + 2 n_{0} \left(\eta_{02} s_{0} s_{1} + \eta_{12} t_{0} t_{1} + \eta_{22} n_{0} n_{1}\right)\right) + s_{0} \left(\eta_{44} n_{0} \left(n_{0} s_{1} + n_{1} s_{0}\right) + \eta_{55} t_{0} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 s_{0} \left(\eta_{00} s_{0} s_{1} + \eta_{01} t_{0} t_{1} + \eta_{02} n_{0} n_{1}\right)\right) + t_{0} \left(\eta_{33} n_{0} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{55} s_{0} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 t_{0} \left(\eta_{01} s_{0} s_{1} + \eta_{11} t_{0} t_{1} + \eta_{12} n_{0} n_{1}\right)\right)\right)\\2 n_{1} \left(\eta_{33} n_{0} t_{0} t_{1} + \eta_{44} n_{0} s_{0} s_{1} + n_{1} \left(\eta_{02} s_{0}^{2} + \eta_{12} t_{0}^{2} + \eta_{22} n_{0}^{2}\right)\right) + 2 s_{1} \left(\eta_{44} n_{0} n_{1} s_{0} + \eta_{55} s_{0} t_{0} t_{1} + s_{1} \left(\eta_{00} s_{0}^{2} + \eta_{01} t_{0}^{2} + \eta_{02} n_{0}^{2}\right)\right) + 2 t_{1} \left(\eta_{33} n_{0} n_{1} t_{0} + \eta_{55} s_{0} s_{1} t_{0} + t_{1} \left(\eta_{01} s_{0}^{2} + \eta_{11} t_{0}^{2} + \eta_{12} n_{0}^{2}\right)\right) & 2 \eta_{00} s_{1}^{4} + 4 \eta_{01} s_{1}^{2} t_{1}^{2} + 4 \eta_{02} n_{1}^{2} s_{1}^{2} + 2 \eta_{11} t_{1}^{4} + 4 \eta_{12} n_{1}^{2} t_{1}^{2} + 2 \eta_{22} n_{1}^{4} + 4 \eta_{33} n_{1}^{2} t_{1}^{2} + 4 \eta_{44} n_{1}^{2} s_{1}^{2} + 4 \eta_{55} s_{1}^{2} t_{1}^{2} & 2 n_{1} \left(\eta_{33} n_{2} t_{1} t_{2} + \eta_{44} n_{2} s_{1} s_{2} + n_{1} \left(\eta_{02} s_{2}^{2} + \eta_{12} t_{2}^{2} + \eta_{22} n_{2}^{2}\right)\right) + 2 s_{1} \left(\eta_{44} n_{1} n_{2} s_{2} + \eta_{55} s_{2} t_{1} t_{2} + s_{1} \left(\eta_{00} s_{2}^{2} + \eta_{01} t_{2}^{2} + \eta_{02} n_{2}^{2}\right)\right) + 2 t_{1} \left(\eta_{33} n_{1} n_{2} t_{2} + \eta_{55} s_{1} s_{2} t_{2} + t_{1} \left(\eta_{01} s_{2}^{2} + \eta_{11} t_{2}^{2} + \eta_{12} n_{2}^{2}\right)\right) & \sqrt{2} \left(n_{1} \left(\eta_{33} t_{1} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{44} s_{1} \left(n_{1} s_{2} + n_{2} s_{1}\right) + 2 n_{1} \left(\eta_{02} s_{1} s_{2} + \eta_{12} t_{1} t_{2} + \eta_{22} n_{1} n_{2}\right)\right) + s_{1} \left(\eta_{44} n_{1} \left(n_{1} s_{2} + n_{2} s_{1}\right) + \eta_{55} t_{1} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 s_{1} \left(\eta_{00} s_{1} s_{2} + \eta_{01} t_{1} t_{2} + \eta_{02} n_{1} n_{2}\right)\right) + t_{1} \left(\eta_{33} n_{1} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{55} s_{1} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 t_{1} \left(\eta_{01} s_{1} s_{2} + \eta_{11} t_{1} t_{2} + \eta_{12} n_{1} n_{2}\right)\right)\right) & \sqrt{2} \left(n_{1} \left(\eta_{33} t_{1} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{44} s_{1} \left(n_{0} s_{2} + n_{2} s_{0}\right) + 2 n_{1} \left(\eta_{02} s_{0} s_{2} + \eta_{12} t_{0} t_{2} + \eta_{22} n_{0} n_{2}\right)\right) + s_{1} \left(\eta_{44} n_{1} \left(n_{0} s_{2} + n_{2} s_{0}\right) + \eta_{55} t_{1} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 s_{1} \left(\eta_{00} s_{0} s_{2} + \eta_{01} t_{0} t_{2} + \eta_{02} n_{0} n_{2}\right)\right) + t_{1} \left(\eta_{33} n_{1} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{55} s_{1} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 t_{1} \left(\eta_{01} s_{0} s_{2} + \eta_{11} t_{0} t_{2} + \eta_{12} n_{0} n_{2}\right)\right)\right) & \sqrt{2} \left(n_{1} \left(\eta_{33} t_{1} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{44} s_{1} \left(n_{0} s_{1} + n_{1} s_{0}\right) + 2 n_{1} \left(\eta_{02} s_{0} s_{1} + \eta_{12} t_{0} t_{1} + \eta_{22} n_{0} n_{1}\right)\right) + s_{1} \left(\eta_{44} n_{1} \left(n_{0} s_{1} + n_{1} s_{0}\right) + \eta_{55} t_{1} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 s_{1} \left(\eta_{00} s_{0} s_{1} + \eta_{01} t_{0} t_{1} + \eta_{02} n_{0} n_{1}\right)\right) + t_{1} \left(\eta_{33} n_{1} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{55} s_{1} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 t_{1} \left(\eta_{01} s_{0} s_{1} + \eta_{11} t_{0} t_{1} + \eta_{12} n_{0} n_{1}\right)\right)\right)\\2 n_{2} \left(\eta_{33} n_{0} t_{0} t_{2} + \eta_{44} n_{0} s_{0} s_{2} + n_{2} \left(\eta_{02} s_{0}^{2} + \eta_{12} t_{0}^{2} + \eta_{22} n_{0}^{2}\right)\right) + 2 s_{2} \left(\eta_{44} n_{0} n_{2} s_{0} + \eta_{55} s_{0} t_{0} t_{2} + s_{2} \left(\eta_{00} s_{0}^{2} + \eta_{01} t_{0}^{2} + \eta_{02} n_{0}^{2}\right)\right) + 2 t_{2} \left(\eta_{33} n_{0} n_{2} t_{0} + \eta_{55} s_{0} s_{2} t_{0} + t_{2} \left(\eta_{01} s_{0}^{2} + \eta_{11} t_{0}^{2} + \eta_{12} n_{0}^{2}\right)\right) & 2 n_{2} \left(\eta_{33} n_{1} t_{1} t_{2} + \eta_{44} n_{1} s_{1} s_{2} + n_{2} \left(\eta_{02} s_{1}^{2} + \eta_{12} t_{1}^{2} + \eta_{22} n_{1}^{2}\right)\right) + 2 s_{2} \left(\eta_{44} n_{1} n_{2} s_{1} + \eta_{55} s_{1} t_{1} t_{2} + s_{2} \left(\eta_{00} s_{1}^{2} + \eta_{01} t_{1}^{2} + \eta_{02} n_{1}^{2}\right)\right) + 2 t_{2} \left(\eta_{33} n_{1} n_{2} t_{1} + \eta_{55} s_{1} s_{2} t_{1} + t_{2} \left(\eta_{01} s_{1}^{2} + \eta_{11} t_{1}^{2} + \eta_{12} n_{1}^{2}\right)\right) & 2 \eta_{00} s_{2}^{4} + 4 \eta_{01} s_{2}^{2} t_{2}^{2} + 4 \eta_{02} n_{2}^{2} s_{2}^{2} + 2 \eta_{11} t_{2}^{4} + 4 \eta_{12} n_{2}^{2} t_{2}^{2} + 2 \eta_{22} n_{2}^{4} + 4 \eta_{33} n_{2}^{2} t_{2}^{2} + 4 \eta_{44} n_{2}^{2} s_{2}^{2} + 4 \eta_{55} s_{2}^{2} t_{2}^{2} & \sqrt{2} \left(n_{2} \left(\eta_{33} t_{2} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{44} s_{2} \left(n_{1} s_{2} + n_{2} s_{1}\right) + 2 n_{2} \left(\eta_{02} s_{1} s_{2} + \eta_{12} t_{1} t_{2} + \eta_{22} n_{1} n_{2}\right)\right) + s_{2} \left(\eta_{44} n_{2} \left(n_{1} s_{2} + n_{2} s_{1}\right) + \eta_{55} t_{2} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 s_{2} \left(\eta_{00} s_{1} s_{2} + \eta_{01} t_{1} t_{2} + \eta_{02} n_{1} n_{2}\right)\right) + t_{2} \left(\eta_{33} n_{2} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{55} s_{2} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 t_{2} \left(\eta_{01} s_{1} s_{2} + \eta_{11} t_{1} t_{2} + \eta_{12} n_{1} n_{2}\right)\right)\right) & \sqrt{2} \left(n_{2} \left(\eta_{33} t_{2} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{44} s_{2} \left(n_{0} s_{2} + n_{2} s_{0}\right) + 2 n_{2} \left(\eta_{02} s_{0} s_{2} + \eta_{12} t_{0} t_{2} + \eta_{22} n_{0} n_{2}\right)\right) + s_{2} \left(\eta_{44} n_{2} \left(n_{0} s_{2} + n_{2} s_{0}\right) + \eta_{55} t_{2} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 s_{2} \left(\eta_{00} s_{0} s_{2} + \eta_{01} t_{0} t_{2} + \eta_{02} n_{0} n_{2}\right)\right) + t_{2} \left(\eta_{33} n_{2} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{55} s_{2} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 t_{2} \left(\eta_{01} s_{0} s_{2} + \eta_{11} t_{0} t_{2} + \eta_{12} n_{0} n_{2}\right)\right)\right) & \sqrt{2} \left(n_{2} \left(\eta_{33} t_{2} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{44} s_{2} \left(n_{0} s_{1} + n_{1} s_{0}\right) + 2 n_{2} \left(\eta_{02} s_{0} s_{1} + \eta_{12} t_{0} t_{1} + \eta_{22} n_{0} n_{1}\right)\right) + s_{2} \left(\eta_{44} n_{2} \left(n_{0} s_{1} + n_{1} s_{0}\right) + \eta_{55} t_{2} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 s_{2} \left(\eta_{00} s_{0} s_{1} + \eta_{01} t_{0} t_{1} + \eta_{02} n_{0} n_{1}\right)\right) + t_{2} \left(\eta_{33} n_{2} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{55} s_{2} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 t_{2} \left(\eta_{01} s_{0} s_{1} + \eta_{11} t_{0} t_{1} + \eta_{12} n_{0} n_{1}\right)\right)\right)\\\sqrt{2} \cdot \left(2 n_{1} \left(\eta_{33} n_{0} t_{0} t_{2} + \eta_{44} n_{0} s_{0} s_{2} + n_{2} \left(\eta_{02} s_{0}^{2} + \eta_{12} t_{0}^{2} + \eta_{22} n_{0}^{2}\right)\right) + 2 s_{1} \left(\eta_{44} n_{0} n_{2} s_{0} + \eta_{55} s_{0} t_{0} t_{2} + s_{2} \left(\eta_{00} s_{0}^{2} + \eta_{01} t_{0}^{2} + \eta_{02} n_{0}^{2}\right)\right) + 2 t_{1} \left(\eta_{33} n_{0} n_{2} t_{0} + \eta_{55} s_{0} s_{2} t_{0} + t_{2} \left(\eta_{01} s_{0}^{2} + \eta_{11} t_{0}^{2} + \eta_{12} n_{0}^{2}\right)\right)\right) & \sqrt{2} \cdot \left(2 n_{1} \left(\eta_{33} n_{1} t_{1} t_{2} + \eta_{44} n_{1} s_{1} s_{2} + n_{2} \left(\eta_{02} s_{1}^{2} + \eta_{12} t_{1}^{2} + \eta_{22} n_{1}^{2}\right)\right) + 2 s_{1} \left(\eta_{44} n_{1} n_{2} s_{1} + \eta_{55} s_{1} t_{1} t_{2} + s_{2} \left(\eta_{00} s_{1}^{2} + \eta_{01} t_{1}^{2} + \eta_{02} n_{1}^{2}\right)\right) + 2 t_{1} \left(\eta_{33} n_{1} n_{2} t_{1} + \eta_{55} s_{1} s_{2} t_{1} + t_{2} \left(\eta_{01} s_{1}^{2} + \eta_{11} t_{1}^{2} + \eta_{12} n_{1}^{2}\right)\right)\right) & \sqrt{2} \cdot \left(2 n_{1} n_{2} \left(\eta_{02} s_{2}^{2} + \eta_{12} t_{2}^{2} + \eta_{22} n_{2}^{2} + \eta_{33} t_{2}^{2} + \eta_{44} s_{2}^{2}\right) + 2 s_{1} s_{2} \left(\eta_{00} s_{2}^{2} + \eta_{01} t_{2}^{2} + \eta_{02} n_{2}^{2} + \eta_{44} n_{2}^{2} + \eta_{55} t_{2}^{2}\right) + 2 t_{1} t_{2} \left(\eta_{01} s_{2}^{2} + \eta_{11} t_{2}^{2} + \eta_{12} n_{2}^{2} + \eta_{33} n_{2}^{2} + \eta_{55} s_{2}^{2}\right)\right) & 2 n_{1} \left(\eta_{33} t_{2} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{44} s_{2} \left(n_{1} s_{2} + n_{2} s_{1}\right) + 2 n_{2} \left(\eta_{02} s_{1} s_{2} + \eta_{12} t_{1} t_{2} + \eta_{22} n_{1} n_{2}\right)\right) + 2 s_{1} \left(\eta_{44} n_{2} \left(n_{1} s_{2} + n_{2} s_{1}\right) + \eta_{55} t_{2} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 s_{2} \left(\eta_{00} s_{1} s_{2} + \eta_{01} t_{1} t_{2} + \eta_{02} n_{1} n_{2}\right)\right) + 2 t_{1} \left(\eta_{33} n_{2} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{55} s_{2} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 t_{2} \left(\eta_{01} s_{1} s_{2} + \eta_{11} t_{1} t_{2} + \eta_{12} n_{1} n_{2}\right)\right) & 2 n_{1} \left(\eta_{33} t_{2} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{44} s_{2} \left(n_{0} s_{2} + n_{2} s_{0}\right) + 2 n_{2} \left(\eta_{02} s_{0} s_{2} + \eta_{12} t_{0} t_{2} + \eta_{22} n_{0} n_{2}\right)\right) + 2 s_{1} \left(\eta_{44} n_{2} \left(n_{0} s_{2} + n_{2} s_{0}\right) + \eta_{55} t_{2} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 s_{2} \left(\eta_{00} s_{0} s_{2} + \eta_{01} t_{0} t_{2} + \eta_{02} n_{0} n_{2}\right)\right) + 2 t_{1} \left(\eta_{33} n_{2} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{55} s_{2} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 t_{2} \left(\eta_{01} s_{0} s_{2} + \eta_{11} t_{0} t_{2} + \eta_{12} n_{0} n_{2}\right)\right) & 2 n_{1} \left(\eta_{33} t_{2} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{44} s_{2} \left(n_{0} s_{1} + n_{1} s_{0}\right) + 2 n_{2} \left(\eta_{02} s_{0} s_{1} + \eta_{12} t_{0} t_{1} + \eta_{22} n_{0} n_{1}\right)\right) + 2 s_{1} \left(\eta_{44} n_{2} \left(n_{0} s_{1} + n_{1} s_{0}\right) + \eta_{55} t_{2} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 s_{2} \left(\eta_{00} s_{0} s_{1} + \eta_{01} t_{0} t_{1} + \eta_{02} n_{0} n_{1}\right)\right) + 2 t_{1} \left(\eta_{33} n_{2} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{55} s_{2} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 t_{2} \left(\eta_{01} s_{0} s_{1} + \eta_{11} t_{0} t_{1} + \eta_{12} n_{0} n_{1}\right)\right)\\\sqrt{2} \cdot \left(2 n_{0} \left(\eta_{33} n_{0} t_{0} t_{2} + \eta_{44} n_{0} s_{0} s_{2} + n_{2} \left(\eta_{02} s_{0}^{2} + \eta_{12} t_{0}^{2} + \eta_{22} n_{0}^{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{0} n_{2} s_{0} + \eta_{55} s_{0} t_{0} t_{2} + s_{2} \left(\eta_{00} s_{0}^{2} + \eta_{01} t_{0}^{2} + \eta_{02} n_{0}^{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{0} n_{2} t_{0} + \eta_{55} s_{0} s_{2} t_{0} + t_{2} \left(\eta_{01} s_{0}^{2} + \eta_{11} t_{0}^{2} + \eta_{12} n_{0}^{2}\right)\right)\right) & \sqrt{2} \cdot \left(2 n_{0} \left(\eta_{33} n_{1} t_{1} t_{2} + \eta_{44} n_{1} s_{1} s_{2} + n_{2} \left(\eta_{02} s_{1}^{2} + \eta_{12} t_{1}^{2} + \eta_{22} n_{1}^{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{1} n_{2} s_{1} + \eta_{55} s_{1} t_{1} t_{2} + s_{2} \left(\eta_{00} s_{1}^{2} + \eta_{01} t_{1}^{2} + \eta_{02} n_{1}^{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{1} n_{2} t_{1} + \eta_{55} s_{1} s_{2} t_{1} + t_{2} \left(\eta_{01} s_{1}^{2} + \eta_{11} t_{1}^{2} + \eta_{12} n_{1}^{2}\right)\right)\right) & \sqrt{2} \cdot \left(2 n_{0} n_{2} \left(\eta_{02} s_{2}^{2} + \eta_{12} t_{2}^{2} + \eta_{22} n_{2}^{2} + \eta_{33} t_{2}^{2} + \eta_{44} s_{2}^{2}\right) + 2 s_{0} s_{2} \left(\eta_{00} s_{2}^{2} + \eta_{01} t_{2}^{2} + \eta_{02} n_{2}^{2} + \eta_{44} n_{2}^{2} + \eta_{55} t_{2}^{2}\right) + 2 t_{0} t_{2} \left(\eta_{01} s_{2}^{2} + \eta_{11} t_{2}^{2} + \eta_{12} n_{2}^{2} + \eta_{33} n_{2}^{2} + \eta_{55} s_{2}^{2}\right)\right) & 2 n_{0} \left(\eta_{33} t_{2} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{44} s_{2} \left(n_{1} s_{2} + n_{2} s_{1}\right) + 2 n_{2} \left(\eta_{02} s_{1} s_{2} + \eta_{12} t_{1} t_{2} + \eta_{22} n_{1} n_{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{2} \left(n_{1} s_{2} + n_{2} s_{1}\right) + \eta_{55} t_{2} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 s_{2} \left(\eta_{00} s_{1} s_{2} + \eta_{01} t_{1} t_{2} + \eta_{02} n_{1} n_{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{2} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{55} s_{2} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 t_{2} \left(\eta_{01} s_{1} s_{2} + \eta_{11} t_{1} t_{2} + \eta_{12} n_{1} n_{2}\right)\right) & 2 n_{0} \left(\eta_{33} t_{2} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{44} s_{2} \left(n_{0} s_{2} + n_{2} s_{0}\right) + 2 n_{2} \left(\eta_{02} s_{0} s_{2} + \eta_{12} t_{0} t_{2} + \eta_{22} n_{0} n_{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{2} \left(n_{0} s_{2} + n_{2} s_{0}\right) + \eta_{55} t_{2} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 s_{2} \left(\eta_{00} s_{0} s_{2} + \eta_{01} t_{0} t_{2} + \eta_{02} n_{0} n_{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{2} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{55} s_{2} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 t_{2} \left(\eta_{01} s_{0} s_{2} + \eta_{11} t_{0} t_{2} + \eta_{12} n_{0} n_{2}\right)\right) & 2 n_{0} \left(\eta_{33} t_{2} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{44} s_{2} \left(n_{0} s_{1} + n_{1} s_{0}\right) + 2 n_{2} \left(\eta_{02} s_{0} s_{1} + \eta_{12} t_{0} t_{1} + \eta_{22} n_{0} n_{1}\right)\right) + 2 s_{0} \left(\eta_{44} n_{2} \left(n_{0} s_{1} + n_{1} s_{0}\right) + \eta_{55} t_{2} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 s_{2} \left(\eta_{00} s_{0} s_{1} + \eta_{01} t_{0} t_{1} + \eta_{02} n_{0} n_{1}\right)\right) + 2 t_{0} \left(\eta_{33} n_{2} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{55} s_{2} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 t_{2} \left(\eta_{01} s_{0} s_{1} + \eta_{11} t_{0} t_{1} + \eta_{12} n_{0} n_{1}\right)\right)\\\sqrt{2} \cdot \left(2 n_{0} \left(\eta_{33} n_{0} t_{0} t_{1} + \eta_{44} n_{0} s_{0} s_{1} + n_{1} \left(\eta_{02} s_{0}^{2} + \eta_{12} t_{0}^{2} + \eta_{22} n_{0}^{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{0} n_{1} s_{0} + \eta_{55} s_{0} t_{0} t_{1} + s_{1} \left(\eta_{00} s_{0}^{2} + \eta_{01} t_{0}^{2} + \eta_{02} n_{0}^{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{0} n_{1} t_{0} + \eta_{55} s_{0} s_{1} t_{0} + t_{1} \left(\eta_{01} s_{0}^{2} + \eta_{11} t_{0}^{2} + \eta_{12} n_{0}^{2}\right)\right)\right) & \sqrt{2} \cdot \left(2 n_{0} n_{1} \left(\eta_{02} s_{1}^{2} + \eta_{12} t_{1}^{2} + \eta_{22} n_{1}^{2} + \eta_{33} t_{1}^{2} + \eta_{44} s_{1}^{2}\right) + 2 s_{0} s_{1} \left(\eta_{00} s_{1}^{2} + \eta_{01} t_{1}^{2} + \eta_{02} n_{1}^{2} + \eta_{44} n_{1}^{2} + \eta_{55} t_{1}^{2}\right) + 2 t_{0} t_{1} \left(\eta_{01} s_{1}^{2} + \eta_{11} t_{1}^{2} + \eta_{12} n_{1}^{2} + \eta_{33} n_{1}^{2} + \eta_{55} s_{1}^{2}\right)\right) & \sqrt{2} \cdot \left(2 n_{0} \left(\eta_{33} n_{2} t_{1} t_{2} + \eta_{44} n_{2} s_{1} s_{2} + n_{1} \left(\eta_{02} s_{2}^{2} + \eta_{12} t_{2}^{2} + \eta_{22} n_{2}^{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{1} n_{2} s_{2} + \eta_{55} s_{2} t_{1} t_{2} + s_{1} \left(\eta_{00} s_{2}^{2} + \eta_{01} t_{2}^{2} + \eta_{02} n_{2}^{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{1} n_{2} t_{2} + \eta_{55} s_{1} s_{2} t_{2} + t_{1} \left(\eta_{01} s_{2}^{2} + \eta_{11} t_{2}^{2} + \eta_{12} n_{2}^{2}\right)\right)\right) & 2 n_{0} \left(\eta_{33} t_{1} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{44} s_{1} \left(n_{1} s_{2} + n_{2} s_{1}\right) + 2 n_{1} \left(\eta_{02} s_{1} s_{2} + \eta_{12} t_{1} t_{2} + \eta_{22} n_{1} n_{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{1} \left(n_{1} s_{2} + n_{2} s_{1}\right) + \eta_{55} t_{1} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 s_{1} \left(\eta_{00} s_{1} s_{2} + \eta_{01} t_{1} t_{2} + \eta_{02} n_{1} n_{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{1} \left(n_{1} t_{2} + n_{2} t_{1}\right) + \eta_{55} s_{1} \left(s_{1} t_{2} + s_{2} t_{1}\right) + 2 t_{1} \left(\eta_{01} s_{1} s_{2} + \eta_{11} t_{1} t_{2} + \eta_{12} n_{1} n_{2}\right)\right) & 2 n_{0} \left(\eta_{33} t_{1} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{44} s_{1} \left(n_{0} s_{2} + n_{2} s_{0}\right) + 2 n_{1} \left(\eta_{02} s_{0} s_{2} + \eta_{12} t_{0} t_{2} + \eta_{22} n_{0} n_{2}\right)\right) + 2 s_{0} \left(\eta_{44} n_{1} \left(n_{0} s_{2} + n_{2} s_{0}\right) + \eta_{55} t_{1} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 s_{1} \left(\eta_{00} s_{0} s_{2} + \eta_{01} t_{0} t_{2} + \eta_{02} n_{0} n_{2}\right)\right) + 2 t_{0} \left(\eta_{33} n_{1} \left(n_{0} t_{2} + n_{2} t_{0}\right) + \eta_{55} s_{1} \left(s_{0} t_{2} + s_{2} t_{0}\right) + 2 t_{1} \left(\eta_{01} s_{0} s_{2} + \eta_{11} t_{0} t_{2} + \eta_{12} n_{0} n_{2}\right)\right) & 2 n_{0} \left(\eta_{33} t_{1} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{44} s_{1} \left(n_{0} s_{1} + n_{1} s_{0}\right) + 2 n_{1} \left(\eta_{02} s_{0} s_{1} + \eta_{12} t_{0} t_{1} + \eta_{22} n_{0} n_{1}\right)\right) + 2 s_{0} \left(\eta_{44} n_{1} \left(n_{0} s_{1} + n_{1} s_{0}\right) + \eta_{55} t_{1} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 s_{1} \left(\eta_{00} s_{0} s_{1} + \eta_{01} t_{0} t_{1} + \eta_{02} n_{0} n_{1}\right)\right) + 2 t_{0} \left(\eta_{33} n_{1} \left(n_{0} t_{1} + n_{1} t_{0}\right) + \eta_{55} s_{1} \left(s_{0} t_{1} + s_{1} t_{0}\right) + 2 t_{1} \left(\eta_{01} s_{0} s_{1} + \eta_{11} t_{0} t_{1} + \eta_{12} n_{0} n_{1}\right)\right)\end{matrix}\right]$$ +# +# + +# + +## I can't see much benefit in expanding the full orthorhombic model +## in this way - keeping the rotations separated makes more sense. + +eta_00 = sympy.symbols(r"\eta_00") +eta_11 = sympy.symbols(r"\eta_11") +eta_22 = sympy.symbols(r"\eta_22") +eta_33 = sympy.symbols(r"\eta_33") +eta_44 = sympy.symbols(r"\eta_44") +eta_55 = sympy.symbols(r"\eta_55") +eta_01 = sympy.symbols(r"\eta_01") +eta_02 = sympy.symbols(r"\eta_02") +eta_12 = sympy.symbols(r"\eta_12") + +I_ijkl = uw.maths.tensor.rank4_identity(3) * 0 +C_IJm_ORTHO = uw.maths.tensor.rank4_to_mandel(I_ijkl, 3) + +C_IJm_ORTHO[0,0] = 2*eta_00 +C_IJm_ORTHO[1,1] = 2*eta_11 +C_IJm_ORTHO[2,2] = 2*eta_22 +C_IJm_ORTHO[3,3] = 2*eta_33 # yz +C_IJm_ORTHO[4,4] = 2*eta_44 # xz +C_IJm_ORTHO[5,5] = 2*eta_55 # xy +C_IJm_ORTHO[0,1] = C_IJm_ORTHO[1,0] = 2*eta_01 +C_IJm_ORTHO[0,2] = C_IJm_ORTHO[2,0] = 2*eta_02 +C_IJm_ORTHO[1,2] = C_IJm_ORTHO[2,1] = 2*eta_12 + +C_ijkl_ORTHO = uw.maths.tensor.mandel_to_rank4(C_IJm_ORTHO, 3) +C_IJv_ORTHO = sympy.simplify(uw.maths.tensor.rank4_to_voigt(C_ijkl_ORTHO, 3)) + +C_IJv_ORTHO + + +# + +# Rotation: Use 3 orthogonal unit vectors to define cannonical orientation + +n = sympy.Matrix(sympy.symarray("n",(3,))) +s = sympy.Matrix(sympy.symarray("s",(3,))) +t = sympy.Matrix(sympy.symarray("t",(3,))) + +# This would work but is less clear in terms of notation +# t = -mesh3.vector.cross(n,s).T # complete the coordinate triad + +Rx = sympy.BlockMatrix((s,t,n)).as_explicit() + +Rx + +# + +C_ijkl_ORTHO = uw.maths.tensor.mandel_to_rank4(C_IJm_ORTHO, 3) +C_ijkl_ORTHO_R = sympy.simplify(uw.maths.tensor.tensor_rotation(Rx, C_ijkl_ORTHO)) +uw.maths.tensor.rank4_to_mandel(C_ijkl_ORTHO_R, 3) + +# print(sympy.latex(uw.maths.tensor.rank4_to_mandel(C_ijkl_ORTHO_R, 3))) + + +# + jupyter={"outputs_hidden": true} + +xi_0 = sympy.symbols(r"\xi_0") +xi_1 = sympy.symbols(r"\xi_1") + +I_ijkl = uw.maths.tensor.rank4_identity(3) * 0 +C_IJm_BC = uw.maths.tensor.rank4_to_mandel(I_ijkl, 3) + +C_IJm_BC[0,0] = 2*xi_0 +C_IJm_BC[1,1] = -2*xi_0 +C_IJm_BC[3,3] = -2*xi_1 # yz +C_IJm_BC[4,4] = 2*xi_1 # xz +C_IJm_BC[5,5] = 0 +C_IJm_BC[0,2] = C_IJm_BC[2,0] = 2*xi_1 +C_IJm_BC[1,2] = C_IJm_BC[2,1] = -2*xi_1 + +C_ijkl_BC = uw.maths.tensor.mandel_to_rank4(C_IJm_BC, 3) +C_IJv_BC = sympy.simplify(uw.maths.tensor.rank4_to_voigt(C_ijkl_BC, 3)) + +print(sympy.latex(C_IJm_BC)) + +C_IJv_BC + +# Rotation by Rx + +C_ijkl_BC = uw.maths.tensor.mandel_to_rank4(C_IJm_BC, 3) +C_ijkl_BC_R = sympy.simplify(uw.maths.tensor.tensor_rotation(Rx, C_ijkl_ORTHO)) +uw.maths.tensor.rank4_to_mandel(C_ijkl_BC_R, 3) + +print(sympy.latex(uw.maths.tensor.rank4_to_mandel(C_ijkl_BC_R, 3))) + +# - +display(C_ijkl_BC_R) + + + + diff --git a/main/_sources/Notebooks/Examples-Utilities/Ex_BCs_from_meshVariables.py b/main/_sources/Notebooks/Examples-Utilities/Ex_BCs_from_meshVariables.py new file mode 100644 index 0000000..eb1d0d5 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Utilities/Ex_BCs_from_meshVariables.py @@ -0,0 +1,187 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Cylindrical 2D Diffusion + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +from petsc4py import PETSc +import underworld3 as uw +from underworld3.systems import Poisson +import numpy as np + +options = PETSc.Options() + +options["dm_plex_check_all"] = None +options["poisson_ksp_rtol"] = 1.0e-3 +# options["poisson_ksp_monitor_short"] = None +# options["poisson_nes_type"] = "fas" +options["poisson_snes_converged_reason"] = None +options["poisson_snes_monitor_short"] = None +# options["poisson_snes_view"]=None +options["poisson_snes_rtol"] = 1.0e-3 + +import os + +os.environ["SYMPY_USE_CACHE"] = "no" + +# - + +# Set some things +k = 1.0 +h = 5.0 +t_i = 2.0 +t_o = 1.0 +r_i = 0.5 +r_o = 1.0 + +# + +dim = 2 +radius_inner = 0.1 +radius_outer = 1.0 + +import pygmsh + +# Generate local mesh. +with pygmsh.occ.Geometry() as geom: + geom.characteristic_length_max = 0.1 + if dim == 2: + ndimspherefunc = geom.add_disk + else: + ndimspherefunc = geom.add_ball + ball_outer = ndimspherefunc( + [ + 0.0, + ] + * dim, + radius_outer, + ) + + if radius_inner > 0.0: + ball_inner = ndimspherefunc( + [ + 0.0, + ] + * dim, + radius_inner, + ) + geom.boolean_difference(ball_outer, ball_inner) + + pygmesh0 = geom.generate_mesh() + +# - + +import meshio + +mesh = uw.meshing.Annulus(radiusOuter=1.0, radiusInner=0.0, cellSize=0.1) + +mesh.dm.view() + +t_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=3) + +# + +# check the mesh if in a notebook / serial + +if PETSc.Comm.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + + clipped_stack = pvmesh.clip( + origin=(0.0, 0.0, 0.0), normal=(-1, -1, 0), invert=False + ) + + pl = pv.Plotter() + + # pl.add_mesh(pvmesh,'Blue', 'wireframe' ) + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + use_transparency=False, + ) + pl.show() + + +# + +# Create Poisson object + +poisson = uw.systems.Poisson( + mesh, u_Field=t_soln, solver_name="poisson", verbose=True +) + +poisson.constitutive_model = uw.constitutive_models.DiffusionModel +poisson.k = k +poisson.f = 0.0 + +bcs_var = uw.discretisation.MeshVariable("bcs", mesh, 1) + +# + +import sympy + +abs_r = sympy.sqrt(mesh.rvec.dot(mesh.rvec)) +bc = sympy.cos(2.0 * mesh.N.y) + +with mesh.access(bcs_var): + bcs_var.data[:, 0] = uw.function.evaluate(bc, bcs_var.coords) + +poisson.add_dirichlet_bc(bcs_var.sym[0], "Upper", components=0) +poisson.add_dirichlet_bc(-1.0, "Centre", components=0) +# - + +poisson.petsc_options.getAll() + +# + +# poisson._setup_terms() +# - + +poisson.solve() + +# + +# check the mesh if in a notebook / serial + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(mesh) + + pvmesh.point_data["T"] = uw.function.evaluate(t_soln.fn, mesh.data) + + # clipped_stack = pvmesh.clip(origin=(0.001,0.0,0.0), normal=(1, 0, 0), invert=False) + + pl = pv.Plotter() + + # pl.add_mesh(pvmesh,'Blue', 'wireframe' ) + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + use_transparency=False, + ) + pl.show(cpos="xy") +# - +mesh.petsc_save_checkpoint(index=0, meshVars=[t_soln], outputPath='./output/') + + +# + +## We should try the non linear version of this next ... diff --git a/main/_sources/Notebooks/Examples-Utilities/Ex_ConstitutiveTensors.py b/main/_sources/Notebooks/Examples-Utilities/Ex_ConstitutiveTensors.py new file mode 100644 index 0000000..075e5d3 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Utilities/Ex_ConstitutiveTensors.py @@ -0,0 +1,782 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# +# # Constitutive relationships in Underworld (pt 1) +# +# Introduction to how the constitutive relationships in Underworld are formulated. + +import petsc4py +from petsc4py import PETSc + +# %% +import sys + + +# %% +import underworld3 as uw +from underworld3.systems import Poisson +import numpy as np +import sympy + +import pytest +from IPython.display import display # since pytest runs pure python + + +# %% +mesh2 = uw.meshing.UnstructuredSimplexBox(minCoords=(0.0,0.0), + maxCoords=(1.0,1.0), + cellSize=1.0/48.0, + regular=True) + +u2 = uw.discretisation.MeshVariable(r"\mathbf{u2}", mesh2, mesh2.dim, vtype=uw.VarType.VECTOR, degree=2) +p2 = uw.discretisation.MeshVariable(r"p^{(2)}", mesh2, mesh2.dim, vtype=uw.VarType.SCALAR, degree=1) +phi2 = uw.discretisation.MeshVariable(r"\phi^{(2)}", mesh2, 1, vtype=uw.VarType.SCALAR, degree=2) + +mesh3 = uw.meshing.UnstructuredSimplexBox(minCoords=(0.0,0.0,0.0), + maxCoords=(1.0,1.0,1.0), + cellSize=1.0/8.0, + regular=True) + +u3 = uw.discretisation.MeshVariable(r"\mathbf{u3}", mesh3, mesh3.dim, vtype=uw.VarType.VECTOR, degree=2) +p3 = uw.discretisation.MeshVariable(r"p^{(3)}", mesh3, 1, vtype=uw.VarType.SCALAR, degree=1) +phi3 = uw.discretisation.MeshVariable(r"\phi^{(3)}", mesh3, 1, vtype=uw.VarType.SCALAR, degree=2) + +R = sympy.Array(sympy.matrices.rot_axis3(sympy.pi)[0:2,0:2]) +R4 = sympy.Array(sympy.matrices.rot_axis3(sympy.pi/4)[0:2,0:2]) +R2 = sympy.Array(sympy.matrices.rot_axis3(sympy.pi/2)[0:2,0:2]) +R34 = sympy.Array(sympy.matrices.rot_axis3(3*sympy.pi/4)[0:2,0:2]) + +# %% +poisson2 = uw.systems.Poisson(mesh2, u_Field=phi2) +poisson3 = uw.systems.Poisson(mesh3, u_Field=phi3) + +stokes2 = uw.systems.Stokes(mesh2, velocityField=u2, pressureField=p2) +stokes3 = uw.systems.Stokes(mesh3, velocityField=u3, pressureField=p3) + +# %% +stokes2 + +# # Tests + +# The following tests are implemented with pytest. + + + +# + [markdown] magic_args="[markdown]" +# ## Introduction to constitutive models +# +# Constitutive models relate fluxes of a quantity to the gradients of the unknowns. For example, in thermal diffusion, there is a relationship between heat flux and temperature gradients which is known as *Fourier's law* and which involves an empirically determined thermal conductivity. +# +# $$ q_i = k \frac{ \partial T}{\partial x_i} $$ +# +# and in elasticity or fluid flow, we have a relationship between stresses and strain (gradients of the displacements) or strain-rate (gradients of velocity) respectively +# +# $$ \tau_{ij} = \mu \left( \frac{\partial u_i}{\partial x_j} + \frac{\partial u_j}{\partial x_i} \right) $$ +# +# where $\eta$, here, is a material property (elastic shear modulus, viscosity) that we need to determine experimentally. +# +# These material properties can be non-linear (they depend upon the quantities that they relate), and they can also be anisotropic (they vary with the orientation of the material). In the latter case, the material property is described through a *consitutive tensor*. For example: +# +# $$ q_i = k_{ij} \frac{ \partial T}{\partial x_j} $$ +# +# or +# +# $$ \tau_{ij} = \mu_{ijkl} \left( \frac{\partial u_k}{\partial x_l} + \frac{\partial u_l}{\partial x_k} \right) $$ + +# + [markdown] magic_args="[markdown]" +# ## Constitutive tensors +# +# In Underworld, the underlying implementation of any consitutive relationship is through the appropriate tensor representation, allowing for a very general formulation for most problems. The constitutive tensors are described symbolically using `sympy` expressions. For scalar equations, the rank 2 constitutive tensor ($k_{ij}$) can be expressed as a matrix without any loss of generality. +# +# For vector equations, the constitutive tensor is of rank 4, but it is common in the engineering / finite element literature to transform the rank 4 tensor to a matrix representation. This has some benefits in terms of legibility and, perhaps, results in common code patterns across the formulation for scalar equations. +# +# We can write the stress and strain rate tensors ($\tau_{ij}$ and $\dot\epsilon_{ij}$ respectively) as +# +# $$ \tau_I = \left( \tau_{00}, \tau_{11}, \tau_{22}, \tau_{12}, \tau_{02}, \tau_{01} \right) $$ +# +# $$ \dot\varepsilon_I = \left( \dot\varepsilon_{00}, \dot\varepsilon_{11}, \dot\varepsilon_{22}, \dot\varepsilon_{12}, \dot\varepsilon_{02}, \dot\varepsilon_{01} \right) $$ +# +# where $I$ indexes $\{i,j\}$ in a specified order and symmetry of the tensor implies only six (in 3D) unique entries in the nine entries of a general tensor. With these definitions, the constitutive tensor can be written as $C_{IJ}$ where $I$, and $J$ range over the independent degrees of freedom in stress and strain (rate). This is a re-writing of the full tensor, with the necessary symmetry required to respect the symmetry of $\tau$ and $\dot\varepsilon$ +# +# The simplified notation comes at a cost. For example, $\tau_I \dot\varepsilon_I$ is not equivalent to the tensor inner produce $\tau_{ij} \dot\varepsilon_{ij}$, and $C_{IJ}$ does not transform correctly for a rotation of coordinates. +# +# However, a modest transformation of the matrix equations is helpful: +# +# $$ \tau^*_{I} = C^*_{IJ} \varepsilon^*_{J} $$ +# +# Where +# +# $$\tau^*_{I} = P_{IJ} \tau^*_J$$ +# +# $$\varepsilon^*_I = P_{IJ} \varepsilon^*_J$$ +# +# $$C^*_{IJ} = P_{IK} C_{KL} P_{LJ}$$ +# +# $\mathbf{P}$ is the scaling matrix +# +# $$P = \left[\begin{matrix}1 & 0 & 0 & 0 & 0 & 0\\0 & 1 & 0 & 0 & 0 & 0\\0 & 0 & 1 & 0 & 0 & 0\\0 & 0 & 0 & \sqrt{2} & 0 & 0\\0 & 0 & 0 & 0 & \sqrt{2} & 0\\0 & 0 & 0 & 0 & 0 & \sqrt{2}\end{matrix}\right]$$ +# +# In this form, known as the Mandel notation, $\tau^*_I\varepsilon^*_I \equiv \tau_{ij} \varepsilon_{ij}$, and the fourth order identity matrix: +# +# $$I_{ijkl} = \frac{1}{2} \left( \delta_{ij}\delta_{kj} + \delta_{kl}\delta_{il} \right)$$ +# +# transforms to +# +# $$I_{IJ} = \delta_{IJ}$$ +# +# +# +# + +# + [markdown] magic_args="[markdown]" +# ## Mandel form, sympy tensorial form, Voigt form +# +# The rank 4 tensor form of the constitutive equations, $c_{ijkl}$, has the following representation in `sympy`: +# +# $$\left[\begin{matrix}\left[\begin{matrix}c_{0 0 0 0} & c_{0 0 0 1}\\c_{0 0 1 0} & c_{0 0 1 1}\end{matrix}\right] & \left[\begin{matrix}c_{0 1 0 0} & c_{0 1 0 1}\\c_{0 1 1 0} & c_{0 1 1 1}\end{matrix}\right]\\\left[\begin{matrix}c_{1 0 0 0} & c_{1 0 0 1}\\c_{1 0 1 0} & c_{1 0 1 1}\end{matrix}\right] & \left[\begin{matrix}c_{1 1 0 0} & c_{1 1 0 1}\\c_{1 1 1 0} & c_{1 1 1 1}\end{matrix}\right]\end{matrix}\right]$$ +# +# and the inner product $\tau_{ij} = c_{ijkl} \varepsilon_{kl} $ is written +# +# ```python +# tau = sympy.tensorcontraction( +# sympy.tensorcontraction( +# sympy.tensorproduct(c, epsilon),(3,5)),(2,3)) +# ``` +# +# However, the `sympy.Matrix` module allows a much simpler expression +# +# ```python +# tau_star = C_star * epsilon_star +# tau = P.inv() * tau_star +# ``` +# +# which we adopt in `underworld` for the display and manipulation of constitutive tensors. +# +# ### Voigt form +# +# Computation of the stress tensor using $\tau^*_I = C^*_{IJ}\varepsilon^*_J$ is equivalent to the following +# +# $$ \mathbf{P} \mathbf{\tau} = \mathbf{P} \mathbf{C} \mathbf{P} \cdot \mathbf{P} \mathbf{\varepsilon} $$ +# +# multiply by $\mathbf{P}^{-1} $ and collecting terms: +# +# $$ \mathbf{\tau} = \mathbf{C} \mathbf{P}^2 \mathbf{\varepsilon} $$ +# +# This is the Voigt form of the constitutive equations and is generally what you will find in a finite element textbook. The Voigt form of the constitutive matrix is the rearrangement of the rank 4 constitutive tensor (with no scaling), the strain rate vector is usually combined with $\mathbf{P}^2$, and the stress vector is raw. For example: +# +# +# $$ \left[\begin{matrix}\tau_{0 0}\\\tau_{1 1}\\\tau_{0 1}\end{matrix}\right] = +# \left[\begin{matrix}\eta & 0 & 0\\0 & \eta & 0\\0 & 0 & \frac{\eta}{2}\end{matrix}\right] \left[\begin{matrix}\dot\varepsilon_{0 0}\\\dot\varepsilon_{1 1}\\2 \dot\varepsilon_{0 1}\end{matrix}\right] +# $$ +# +# A full discussion of this can be found in Brannon (2018) and Helnwein (2001). +# +# +# ### References. +# +# Brannon, R. (2018). Rotation Reflection and Frame Changes Orthogonal tensors in computational engineering mechanics. IOP Publishing. https://doi.org/10.1088/978-0-7503-1454-1 +# +# Helnwein, P. (2001). Some remarks on the compressed matrix representation of symmetric second-order and fourth-order tensors. Computer Methods in Applied Mechanics and Engineering, 190(22–23), 2753–2770. https://doi.org/10.1016/S0045-7825(00)00263-2 +# +# +# +# - + +# %% +epsdot = uw.maths.tensor.rank2_symmetric_sym("\\dot\\varepsilon", 2) +display(epsdot) +epsdot_vec = uw.maths.tensor.rank2_to_voigt(epsdot, 2) +display(epsdot_vec) +Pm = uw.maths.tensor.P_mandel[2] + +# %% +I4 = uw.maths.tensor.rank4_identity(2) +I4v = uw.maths.tensor.rank4_to_voigt(I4,2) +I4m = uw.maths.tensor.rank4_to_mandel(I4,2) + +display(I4v) +display(I4m) + +# # What does this show then ? + +display(Pm * Pm * epsdot_vec.T) +display(Pm * I4v * Pm * epsdot_vec.T) + +# This is the generic constitutive tensor with relevant symmetries for fluid mechanics problems + +c4sym = uw.maths.tensor.rank4_symmetric_sym('c', 2) +display(c4sym) + +# # This is the mandel form of the constitutive matrix for constant viscosity + +eta = sympy.symbols("\eta") +Ceta = sympy.Matrix.diag([eta]*3) +display(Ceta) + +# And this is the equivalent tensor +display(uw.maths.tensor.mandel_to_rank4(Ceta, 2)) + + +# %% +Cv = uw.maths.tensor.rank4_to_voigt(c4sym,2) +Cm = uw.maths.tensor.rank4_to_mandel(c4sym,2) + +display(Cv) +display(Cm) + +# %% +d=3 +display(uw.maths.tensor.rank4_to_voigt(uw.maths.tensor.rank4_identity(d), d)) +sympy.Array(sympy.symarray('C',(d,d,d,d))) + +# # This is how we use those things + +ViscousFlow = uw.constitutive_models.TransverseIsotropicFlowModel +ViscousFlow.Parameters.eta_0=sympy.symbols(r"\eta_0") +ViscousFlow.Parameters.eta_1=sympy.symbols(r"\eta_1") +ViscousFlow.Parameters.director=sympy.Matrix([1,0,0]).T + + + + +ViscousFlow.flux + +stokes3.constitutive_model = ViscousFlow +display(stokes3.strainrate) +display(stokes3.constitutive_model.flux) + +# %% +ViscousFlow.Parameters.eta_0=sympy.symbols(r"\eta_0") +ViscousFlow.Parameters.eta_1=sympy.symbols(r"\eta_0") +ViscousFlow.Parameters.director=sympy.Matrix([1,0,0]) # Doesn't matter if the viscosity are the same + +stokes3.constitutive_model.flux + +# %% +Cmods = uw.constitutive_models.TransverseIsotropicFlowModel(u3) +Cmods.Parameters.eta_0=sympy.symbols(r"\eta_0") +Cmods.Parameters.eta_1=sympy.symbols(r"\eta_1") + +Cmods.Parameters.director=sympy.Matrix([1,0,0]).T +display(Cmods.C) + +Cmods.Parameters.director=sympy.Matrix([0,1,0]).T +display(Cmods.C) + +# %% +# Description / help is especially useful in notebook form +Cmods.view() + +# %% +gradT = mesh2.vector.gradient(phi2.sym) +Cmodp = uw.constitutive_models.Constitutive_Model(mesh2.dim, 1) +Cmodp + + +# %% +Cmodp.Parameters.k = sympy.symbols("\\kappa") +display(Cmodp.C) +display(Cmodp.c) + +# %% +Cmodp.flux(gradT) + +# %% +Cmods.k = sympy.symbols("\\eta") +Cmods.c + +# %% +Cmods + +# %% +Cmodv = uw.constitutive_models.ViscousFlowModel(2) +Cmodv.Parameters.viscosity = sympy.symbols(r"\eta") +Cmodv + +# %% +Cmodv.flux(epsdot) + +# Cvisc = sympy.symbols(r'\eta') * uw.maths.tensor.rank4_identity(2) +# Celas = sympy.symbols(r'\mu') * uw.maths.tensor.rank4_identity(2) + +# Cvisc + Celas + +# # Equivalence test: define tensor explicitly or in canonical form with rotation + +# + +# Rotation (as defined for Muhlhaus / Moresi transverse isotropy) + +n = sympy.Matrix(sympy.symarray("n",(3,))) +s = sympy.Matrix((-n[1], n[0], 0)) +t = -mesh3.vector.cross(n,s).T # complete the coordinate triad + +# - + +R = sympy.BlockMatrix((s,n,t)).as_explicit() +R + +# # Equivalence test - 2D tensor rotation + +Delta = sympy.symbols(r"\Delta") +lambda_matrix = sympy.Matrix.diag([0,0, Delta]) +lambda_ani = uw.maths.tensor.mandel_to_rank4(lambda_matrix,2) +lambda_xyz = sympy.simplify(uw.maths.tensor.tensor_rotation(R2, lambda_ani)) +lambda_xyz_m = sympy.simplify(uw.maths.tensor.rank4_to_mandel(lambda_xyz, 2)) + + +lambda_matrix + +# # Equivalence test - 2D tensor definition + +d=2 +lambda_mat2 = uw.maths.tensor.rank4_identity(2) * 0 +lambda_mat2 + +for i in range(d): + for j in range(d): + for k in range(d): + for l in range(d): + lambda_mat2[i,j,k,l] = Delta * ((n[i]*n[k]*int(j==l) + n[j]*n[k] * int(l==i) + + n[i]*n[l]*int(j==k) + n[j]*n[l] * int(k==i))/2 + - 2 * n[i]*n[j]*n[k]*n[l] ) + +lambda_mat_m = sympy.simplify(uw.maths.tensor.rank4_to_mandel(lambda_mat2,2)) +difference = sympy.simplify(lambda_mat_m - lambda_xyz_m) + +difference + + +# Are they term-by-term equivalent +sympy.simplify(difference.subs(n[1],sympy.sqrt(1-n[0]**2))) + +n = sympy.Matrix(sympy.symarray("n",(3,))) +s = (sympy.Matrix((-n[1], n[0], 0)) + sympy.Matrix((0, -n[2], n[1])))/2 +s /= sympy.sqrt((s.dot(s))) + +n_ijk = mesh3.vector.to_vector(n.T) +s_ijk = mesh3.vector.to_vector(s.T) +t = mesh3.vector.to_matrix(n_ijk.cross(s_ijk)).T + +R = sympy.BlockMatrix((n,s,t)).as_explicit() +R + +# # Equivalence test - 3D tensor rotation + +Delta = sympy.symbols(r"\Delta") +lambda_matrix = sympy.Matrix.diag([0, 0, 0, 0, Delta , Delta]) +lambda_ani = uw.maths.tensor.mandel_to_rank4(lambda_matrix,3) +lambda_xyz = sympy.simplify(uw.maths.tensor.tensor_rotation(R, lambda_ani)) +lambda_xyz_m = sympy.simplify(uw.maths.tensor.rank4_to_mandel(lambda_xyz, 3)) + +lambda_matrix + + +# # Equivalence test - 3D tensor definition + +d=3 +lambda_mat2 = uw.maths.tensor.rank4_identity(d) * 0 + + + +for i in range(d): + for j in range(d): + for k in range(d): + for l in range(d): + lambda_mat2[i,j,k,l] = Delta * ((n[i]*n[k]*int(j==l) + n[j]*n[k] * int(l==i) + + n[i]*n[l]*int(j==k) + n[j]*n[l] * int(k==i))/2 + - 2 * n[i]*n[j]*n[k]*n[l] ) + +lambda_mat_m = sympy.simplify(uw.maths.tensor.rank4_to_mandel(lambda_mat2,d)) +difference = sympy.simplify(lambda_mat_m - lambda_xyz_m) + +# Are they term-by-term equivalent (should be if n is a unit vector) +sympy.simplify(difference.subs(n[2],sympy.sqrt(1-n[0]**2-n[1]**2))) + + +# + +# Muhlhaus et al + +eta0 = sympy.symbols("\eta_0") +eta1 = sympy.symbols("\eta_1") + +lambda_MM = uw.maths.tensor.rank4_identity(3) * 0 +lambda_MM_mandel = uw.maths.tensor.rank4_to_mandel(lambda_MM, 3) +lambda_MM_mandel[0,0] = eta0 +lambda_MM_mandel[1,1] = eta0 +lambda_MM_mandel[2,2] = eta0 +lambda_MM_mandel[3,3] = eta0 +lambda_MM_mandel[4,4] = eta1 +lambda_MM_mandel[5,5] = eta1 +lambda_MM = uw.maths.tensor.mandel_to_rank4(lambda_MM_mandel, 3) +lambda_xyz = sympy.simplify(uw.maths.tensor.tensor_rotation(R, lambda_MM)) +lambda_xyz_m = sympy.simplify(uw.maths.tensor.rank4_to_mandel(lambda_xyz, 3)) + +# - + +lambda_xyz_m + + + + + + + + + +# Note: the two forms are equivalent, the second is simpler to implement and sympy probably likes it better. + +# %% +0/0 + +# # What does the identity tensor look like in C_ijkl, converted ? + +d = 2 + +I = sympy.MutableDenseNDimArray.zeros(d,d,d,d) + +for i in range(d): + for j in range(d): + for k in range(d): + for l in range(d): + I[i,j,k,l] = eta * sympy.sympify((i==k)*(j==l) + (i==l)*(j==k)) / 2 + +I + +uw.maths.tensor.rank4_to_mandel(I, 2) + +uw.maths.tensor.rank4_to_voigt(I, 2) + +# %% +uw.maths.tensor.voigt_to_rank4(P.inv() * sympy.Matrix.eye(6) * P.inv(),2) + +# %% +E3 = sympy.Matrix(sympy.symarray('\dot\epsilon',(3,3))) +E3[1,0] = E3[0,1] # enforce symmetry +E3[2,0] = E3[0,2] # --- " --- +E3[2,1] = E3[1,2] # --- " --- + +mesh3.tensor.rank2_to_mandel(E3) + +# %% +tau_r = 2 * sympy.tensorcontraction(sympy.tensorcontraction(sympy.tensorproduct(I, E3),(3,5)),(2,3)) + +tau_r + +# %% +0/0 + +# %% +P*mesh3.tensor.rank4_to_voigt(I)*P + +# %% +P * P * Cmd * Em + +# %% +P = sympy.Matrix.diag([1, 1, 1, sympy.sqrt(2), sympy.sqrt(2), sympy.sqrt(2)]) +eta = sympy.symbols("\eta") +C = sympy.Matrix.diag([eta]*6) + +# %% +Pm = + +Cv + +# %% +P * Cmods.template_L # this is the strain rate in Mandel form + +# %% +0/0 + +# %% +0/0 + +# %% +sympy.Matrix(sympy.symarray('\partial\phi,',(d,))) + + +# %% +M = sympy.Matrix(sympy.symarray("\\left(\\nabla\\mathbf{u}\\right)",(3,))) +M + +# %% +Mt = sympy.Matrix(sympy.symarray("\\left(\\nabla\\mathbf{u^*} + \\nabla\\mathbf{u^*}^T\\right)",(3,))) +Mt + +# %% +C.is_symmetric() + +# %% +isinstance(stokes, uw.systems.SNES_Vector) + +# %% +# Set some things +poisson.k = 1. +poisson.f = 0. +poisson.add_dirichlet_bc( 1., "Bottom" ) +poisson.add_dirichlet_bc( 0., "Top" ) + +# %% +poisson._setup_terms() +poisson.snes.setTolerances(rtol=1.0e-6) +poisson.snes.ksp.monitorCancel() + +# %% +# Solve time +poisson.solve() + +# %% +display(poisson._f1) +display(poisson._L) + + +# %% +Cmod = uw.constitutive_models.Constitutive_Model(poisson) + +# %% +Cmod.template_c + +# %% +Cmod3 = uw.constitutive_models.Constitutive_Model(poisson3) +Cmod3.template_c + +# %% +Cmods = uw.constitutive_models.Constitutive_Model(stokes) + +# %% +Cmods.template_c + +# %% +mesh.tensor.rank4_to_voigt(Cmods.template_c) + +# %% +import sympy +K = sympy.Matrix.eye(2,2) +K[0,0] = 100 + +# %% +R = sympy.matrices.rot_axis3(sympy.pi)[0:2,0:2] + +# %% +K + +# %% +K1 = R.T * K * R + +# %% +(K1 * poisson._L.T).T + +# + [markdown] magic_args="[markdown]" +# https://farside.ph.utexas.edu/teaching/336L/Fluidhtml/node250.html +# +# +# Would be useful to demonstrate how to do tensorproduct / tensor contraction rotation of a tensor +# to make sure it works for rank 4 case. +# +# Define ${\cal R}_{ij}$ as the rotation matrix that maps $x$ onto the $x' $ coordiate system, i.e. +# +# +# $\displaystyle a_i' = {\cal R}_{ij}\,a_j,$, for vectors, +# +# which also has this property: +# +# $\displaystyle {\cal R}_{ki}\,{\cal R}_{kj } = \delta_{ij}.$ +# +# Second order tensors transform as follows: +# +# $\displaystyle a_{ij}' = {\cal R}_{ik}\,{\cal R}_{jl}\,a_{kl},$ +# +# and for higher rank tensors, we just continue ... +# +# $\displaystyle a_{ijk}' = {\cal R}_{il}\,{\cal R}_{jm}\,{\cal R}_{kn}\,a_{lmn}.$ +# +# +# - + +# 2D example - this should be one of the tests. + +R = sympy.Array(sympy.matrices.rot_axis3(sympy.pi)[0:2,0:2]) +R4 = sympy.Array(sympy.matrices.rot_axis3(sympy.pi/4)[0:2,0:2]) +R2 = sympy.Array(sympy.matrices.rot_axis3(sympy.pi/2)[0:2,0:2]) +R34 = sympy.Array(sympy.matrices.rot_axis3(3*sympy.pi/4)[0:2,0:2]) + + +display(R) +display(R2) +display(R4) +display(R34) + + +# %% +def tensor_rotation(R, T): + rank = T.rank() + print("Rank = ",rank) + + # Tc = T.copy() + + for i in range(rank): + T = sympy.tensorcontraction(sympy.tensorproduct(R,T),(1,rank+1)) + + return T + + +A = sympy.Array(sympy.symarray('a',(2,2))) +C = sympy.Array(sympy.symarray('c',(2,2,2,2))) +V = sympy.Array(sympy.symarray('v',(2,))) + +A3 = sympy.Array(sympy.symarray('a',(3,3))) +C3 = sympy.Array(sympy.symarray('c',(3,3,3,3))) +V3 = sympy.Array(sympy.symarray('v',(3,))) + + +# %% +CC = sympy.MutableDenseNDimArray(0 * C) +eta0 = sympy.symbols("\eta_0") +eta1 = sympy.symbols("\eta_1") + +CC[0,0,0,0] = CC[1,1,1,1] = 2 * eta0 +CC[0,1,0,1] = CC[1,0,1,0] = eta1 +CC[0,1,1,0] = CC[1,0,0,1] = eta1 + + +E = sympy.MutableDenseNDimArray(sympy.symarray('\dot\epsilon',(2,2))) +E[0,1] = E[1,0] # enforce symmetry + + + +CC3 = sympy.MutableDenseNDimArray(0 * C3) +eta30 = sympy.symbols("\eta_0") +eta31 = sympy.symbols("\eta_1") + +CC3[0,0,0,0] = CC3[1,1,1,1] = CC3[2,2,2,2] = 2 * eta0 + +CC3[0,1,0,1] = CC3[1,0,1,0] = eta1 +CC3[0,1,1,0] = CC3[1,0,0,1] = eta1 + +CC3[1,2,1,2] = CC3[2,1,2,1] = eta1 +CC3[2,1,1,2] = CC3[1,2,2,1] = eta1 + +CC3[0,2,0,2] = CC3[2,0,2,0] = eta0 +CC3[2,0,0,2] = CC3[0,2,2,0] = eta0 + +E3 = sympy.MutableDenseNDimArray(sympy.symarray('\dot\epsilon',(3,3))) +E3[1,0] = E3[0,1] # enforce symmetry +E3[2,0] = E3[0,2] # --- " --- +E3[2,1] = E3[1,2] # --- " --- + +# %% +P = sympy.Matrix.zeros(6,6) +P[0,0] = P[1,1] = P[2,2] = 1 +P[3,3] = P[4,4] = P[5,5] = 2 + +# %% +tau_v = Cv * P * Ev.T +tau_v + +# %% +display(voigt_to_rank2(tau_v)) +mesh3.tensor.voigt_to_rank2(tau_v) + +# %% +tau = sympy.tensorcontraction(sympy.tensorcontraction(sympy.tensorproduct(CC3, E3),(3,5)),(2,3)) +display(tau) + +# %% +CCr = tensor_rotation(R34, CC) +tau_r = sympy.tensorcontraction(sympy.tensorcontraction(sympy.tensorproduct(CCr, E),(3,5)),(2,3)) +tau_r + +display(sympy.simplify(CCr)) +display(sympy.simplify(tau_r)) + +# %% +display(tensor_rotation(R2, A)) +display(tensor_rotation(R2, V)) + +A2 = tensor_rotation(R2, A) +A3 = tensor_rotation(R2, A2) + + + + +# %% +Crot = tensor_rotation(R2, tensor_rotation(R2, C)) + +# %% +Crot - C + +# %% +C = sympy.tensorcontraction(sympy.tensorproduct(R,C),(1,3)) +C.shape +C + +# %% +sympy.tensorcontraction(sympy.tensorproduct(R,R),(1,2)) + +# %% +sympy.tensorproduct(R,R).shape + +# %% +R.transpose() + +# %% +0/0 + +# Check. Construct simple linear which is solution for +# above config. Exclude boundaries from mesh data. + +import numpy as np +with mesh.access(): + mesh_numerical_soln = uw.function.evaluate(poisson.u.fn, mesh.data) + mesh_analytic_soln = uw.function.evaluate(1.0-mesh.N.y, mesh.data) + if not np.allclose(mesh_analytic_soln, mesh_numerical_soln, rtol=0.01): + raise RuntimeError("Unexpected values encountered.") + +# %% +(mesh_analytic_soln - mesh_numerical_soln).max() + + +# Validate + +from mpi4py import MPI + +if MPI.COMM_WORLD.size==1: + + import numpy as np + import pyvista as pv + import vtk + + pv.global_theme.background = 'white' + pv.global_theme.window_size = [500, 500] + pv.global_theme.antialiasing = True + pv.global_theme.jupyter_backend = 'panel' + pv.global_theme.smooth_shading = True + + mesh.vtk("ignore_box.vtk") + pvmesh = pv.read("ignore_box.vtk") + + + + with mesh.access(): + pvmesh.point_data["T"] = mesh_analytic_soln + pvmesh.point_data["T2"] = mesh_numerical_soln + pvmesh.point_data["DT"] = pvmesh.point_data["T"] - pvmesh.point_data["T2"] + + pl = pv.Plotter() + + pl.add_mesh(pvmesh, cmap="coolwarm", edge_color="Black", show_edges=True, scalars="DT", + use_transparency=False, opacity=0.5) + + pl.show(cpos="xy") + diff --git a/main/_sources/Notebooks/Examples-Utilities/Ex_Integrals_on_Meshes.py b/main/_sources/Notebooks/Examples-Utilities/Ex_Integrals_on_Meshes.py new file mode 100644 index 0000000..cfe6363 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Utilities/Ex_Integrals_on_Meshes.py @@ -0,0 +1,208 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Using the PETSc mesh integration routines +# +# This is probably better moved to become a test ! + +import underworld3 as uw +import numpy as np +import sympy + +# + +mesh = uw.meshing.UnstructuredSimplexBox(minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0), cellSize=1.0 / 32.0, regular=True) +s_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=2) # add this line to avoid petsc error for time being + +x = mesh.N.x +y = mesh.N.y +z = mesh.N.z + +I = uw.maths.Integral(mesh, x * y) +print(I.evaluate()) # 0.25 + + +# + +mesh = uw.meshing.StructuredQuadBox(minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0), elementRes=(32, 32)) +s_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=2) # add this line to avoid petsc error for time being + +x = mesh.N.x +y = mesh.N.y +z = mesh.N.z + +I2 = uw.maths.Integral(mesh, x * y) +print(I2.evaluate()) # 0.25 + +# + +mesh = uw.meshing.StructuredQuadBox(minCoords=(0.0, 0.0, 0.0), maxCoords=(1.0, 1.0, 1.0), elementRes=(8, 8, 8)) +s_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=2) # add this line to avoid petsc error for time being + +x = mesh.N.x +y = mesh.N.y +z = mesh.N.z + +I3 = uw.maths.Integral(mesh, x * y * z) +print(I3.evaluate()) # 0.125 + +# + +mesh = uw.meshing.UnstructuredSimplexBox( + minCoords=(0.0, 0.0, 0.0), maxCoords=(1.0, 1.0, 1.0), cellSize=1.0 / 8.0, regular=True +) +s_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=2) # add this line to avoid petsc error for time being + +x = mesh.N.x +y = mesh.N.y +z = mesh.N.z + +I4 = uw.maths.Integral(mesh, x * y * z) +print(I4.evaluate()) # 0.125 + +# + +mesh = uw.meshing.Annulus(radiusInner=0.5, radiusOuter=1.0, cellSize=0.05) +s_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=2) # add this line to avoid petsc error for time being + +x = mesh.N.x +y = mesh.N.y +z = mesh.N.z + +I5 = uw.maths.Integral(mesh, 1.0) +print(I5.evaluate()) # 3 * pi / 4 = 2.35 + +# + +mesh = uw.meshing.Annulus(radiusInner=0.0, radiusOuter=1.0, cellSize=0.05) +s_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=2) # add this line to avoid petsc error for time being + +x = mesh.N.x +y = mesh.N.y +z = mesh.N.z + +I6 = uw.maths.Integral(mesh, 1.0) +print(I6.evaluate()) # pi + +# + +mesh = uw.meshing.SphericalShell(radiusInner=0.5, radiusOuter=1.0, cellSize=0.2) +s_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=2) # add this line to avoid petsc error for time being + +x = mesh.N.x +y = mesh.N.y +z = mesh.N.z + +I7 = uw.maths.Integral(mesh, 1) +print(I7.evaluate()) # 4/3 * 7/8 * pi + +# + +mesh = uw.meshing.SphericalShell(radiusInner=0.0, radiusOuter=1.0, cellSize=0.2) +s_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=2) # add this line to avoid petsc error for time being + +x = mesh.N.x +y = mesh.N.y +z = mesh.N.z + +I8 = uw.maths.Integral(mesh, 1) +print(I8.evaluate()) # 4/3 * pi + +# + +# mesh = uw.meshing.CubicSphere(radiusInner=0.5, radiusOuter=1.0, numElements=30) +# s_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=2) # add this line to avoid petsc error for time being + +# x = mesh.N.x +# y = mesh.N.y +# z = mesh.N.z + +# I9 = uw.maths.Integral(mesh, 1) +# print(I9.evaluate()) # 4/3 * 7/8 * pi (3.634) + +# + +mesh = uw.meshing.SphericalShell(radiusInner=0.0, radiusOuter=1.0, cellSize=0.5) +# mesh = uw.meshing.Annulus(radiusInner = 0.0, radiusOuter=1.0, cellSize=0.05) +s_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=2) # add this line to avoid petsc error for time being + +x = mesh.N.x +y = mesh.N.y +z = mesh.N.z + +radius_fn = sympy.sqrt(mesh.rvec.dot(mesh.rvec)) # normalise by outer radius if not 1.0 +unit_rvec = mesh.rvec / (1.0e-10 + radius_fn) + +r = sympy.sqrt(x**2 + y**2) +th = sympy.atan2(y + 1.0e-10, x + 1.0e-10) + +v_soln = uw.discretisation.MeshVariable("U", mesh, mesh.dim, degree=2) +p_soln = uw.discretisation.MeshVariable("P", mesh, 1, degree=1) +t_soln = uw.discretisation.MeshVariable("T", mesh, 1, degree=3) + +swarm = uw.swarm.Swarm(mesh=mesh) +v_star = uw.swarm.SwarmVariable("Vs", swarm, mesh.dim, proxy_degree=2) +remeshed = uw.swarm.SwarmVariable("Vw", swarm, 1, dtype="int", _proxy=False) +X_0 = uw.swarm.SwarmVariable("X0", swarm, mesh.dim, _proxy=False) + +swarm.populate(fill_param=4) + +I10 = uw.maths.Integral(mesh, 1) +print(I10.evaluate()) + +stokes = uw.systems.Stokes( + mesh, + velocityField=v_soln, + pressureField=p_soln, + # u_degree=v_soln.degree, + # p_degree=p_soln.degree, + verbose=False, + solver_name="stokes", +) + +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel + +stokes.add_dirichlet_bc((0.0, 0.0, 0.0), "Upper", (0, 1, 2)) + +stokes.viscosity = 1.0 +stokes.bodyforce = mesh.rvec * sympy.sin(th) + + +stokes.solve() + +I10 = uw.maths.Integral(mesh, v_soln.fn.dot(v_soln.fn)) +print(I10.evaluate()) + + +# + +## Check this this sort of thing works OK + +mesh = uw.meshing.SphericalShell(radiusInner=0.0, radiusOuter=1.0, cellSize=0.1) + +x = mesh.N.x +y = mesh.N.y +z = mesh.N.z + +meshvar = uw.discretisation.MeshVariable("phi", mesh, 1, degree=3) + +I = uw.maths.Integral(mesh, 1) +print(I.evaluate()) # 4/3 * pi + +I.fn = 0.75 +print(I.evaluate()) # pi + +I.fn = x +print(I.evaluate()) # should be zero + +I.fn = x + y + z +print(I.evaluate()) # should be zero + +with mesh.access(meshvar): + meshvar.data[:, 0] = uw.function.evaluate(x + y + z, meshvar.coords) + +I.fn = meshvar.fn +print(I.evaluate()) # should be zero +# - + + diff --git a/main/_sources/Notebooks/Examples-Utilities/Ex_Projection_Function_Eval.py b/main/_sources/Notebooks/Examples-Utilities/Ex_Projection_Function_Eval.py new file mode 100644 index 0000000..555d45a --- /dev/null +++ b/main/_sources/Notebooks/Examples-Utilities/Ex_Projection_Function_Eval.py @@ -0,0 +1,136 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Projection-based function evaluation +# +# Here we Use SNES solvers to project sympy / mesh variable functions and derivatives to nodes. Pointwise / symbolic functions cannot always be evaluated using `uw.function.evaluate` because they contain a mix of mesh variables, derivatives and symbols which may not be defined everywhere. +# +# Our solution is to use a projection of the function to a continuous mesh variable with the SNES machinery performing all of the background operations to determine the values and the optimal fitting. +# +# This approach also allows us to include boundary conditions, smoothing, and constraint terms (e.g. remove a null space) in cases (like piecewise continuous swarm variables) where this is difficult in the original form. +# +# We'll demonstrate this using a swarm variable (scalar / vector), but the same approach is useful for gradient recovery. + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +import underworld3 as uw +import numpy as np +import sympy + +from underworld3.meshing import UnstructuredSimplexBox + +meshbox = UnstructuredSimplexBox(minCoords=(0.0, 0.0), + maxCoords=(1.0, 1.0), + cellSize=1.0 / 32.0,) + +# + +# import meshio +# mesh2 = meshio.read(filename="../Examples-StokesFlow/tmp_ball.vtk") +# meshio.write(filename="tmp_ball.msh", mesh=mesh2) + +# + +import sympy + +# Some useful coordinate stuff + +x, y = meshbox.X +# - + +s_soln = uw.discretisation.MeshVariable("T", meshbox, 1, degree=2) + +v_soln = uw.discretisation.MeshVariable("U", meshbox, meshbox.dim, degree=2) + +s_fn = sympy.cos(5.0 * sympy.pi * x) * sympy.cos(5.0 * sympy.pi * y) +v_fn = sympy.Matrix([sympy.cos(5.0 * sympy.pi * y) ** 2, -sympy.sin(5.0 * sympy.pi * x) ** 2]) # divergence free + +meshbox.vector.divergence(v_fn) + +# + +swarm = uw.swarm.Swarm(mesh=meshbox) +s_values = uw.swarm.SwarmVariable("Ss", swarm, 1, proxy_degree=3) +v_values = uw.swarm.SwarmVariable("Vs", swarm, meshbox.dim, proxy_degree=3) +# iv_values = uw.swarm.SwarmVariable("Vi", swarm, meshbox.dim, proxy_degree=3) + +swarm.populate(fill_param=3) +# - + + +scalar_projection = uw.systems.Projection(meshbox, s_soln) +scalar_projection.uw_function = s_values.sym +scalar_projection.smoothing = 1.0e-6 + + +# + +vector_projection = uw.systems.Vector_Projection(meshbox, v_soln) +vector_projection.uw_function = v_values.sym +vector_projection.smoothing = 1.0e-6 # see how well it works ! +vector_projection.penalty = 1.0e-6 + +# Velocity boundary conditions (compare left / right walls in the soln !) + +vector_projection.add_dirichlet_bc(v_fn, "Left", (0, 1)) +vector_projection.add_dirichlet_bc(v_fn, "Right", (0, 1)) +vector_projection.add_dirichlet_bc(v_fn, "Top", (0, 1)) +vector_projection.add_dirichlet_bc(v_fn, "Bottom", (0, 1)) +# - + +with swarm.access(s_values, v_values): + s_values.data[:, 0] = uw.function.evaluate(s_fn, swarm.data, meshbox.N) + v_values.data[:, 0] = uw.function.evaluate(v_fn[0], swarm.data, meshbox.N) + v_values.data[:, 1] = uw.function.evaluate(v_fn[1], swarm.data, meshbox.N) + + +scalar_projection.solve() + +vector_projection.solve() + +scalar_projection.uw_function = meshbox.vector.divergence(v_soln.sym) +scalar_projection.solve() + +s_soln.stats() + +# + +# check the projection + + +if uw.mpi.size == 1: + + import pyvista as pv + import underworld3.visualisation as vis + + pvmesh = vis.mesh_to_pv_mesh(meshbox) + pvmesh.point_data["S"] = vis.scalar_fn_to_pv_points(pvmesh, s_soln.sym) + + velocity_points = vis.meshVariable_to_pv_cloud(v_soln) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v_soln.sym) + + pl = pv.Plotter(window_size=(1000, 750)) + + # pl.add_mesh(pvmesh,'Black', 'wireframe') + + pl.add_mesh( + pvmesh, cmap="coolwarm", edge_color="Black", show_edges=True, scalars="S", use_transparency=False, opacity=0.5 + ) + + pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=1.0e-1, opacity=0.5) + # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1) + + # pl.add_points(pdata) + + pl.show(cpos="xy") +# - + + diff --git a/main/_sources/Notebooks/Examples-Utilities/Ex_Stokes_Annulus_Benchmark_Kramer_etal.py b/main/_sources/Notebooks/Examples-Utilities/Ex_Stokes_Annulus_Benchmark_Kramer_etal.py new file mode 100644 index 0000000..184a4b2 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Utilities/Ex_Stokes_Annulus_Benchmark_Kramer_etal.py @@ -0,0 +1,344 @@ +# ## Annulus Benchmark: Isoviscous Incompressible Stokes +# +# ### Case: Infinitely thin density anomaly at $r = r'$ +# #### [Benchmark paper](https://gmd.copernicus.org/articles/14/1899/2021/) +# +# *Author: [Thyagarajulu Gollapalli](https://github.com/gthyagi)* + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import underworld3 as uw +from underworld3.systems import Stokes + +import numpy as np +import sympy +from sympy import lambdify +import os +import matplotlib.pyplot as plt +import cmcrameri.cm as cmc +import assess +# - + +os.environ["SYMPY_USE_CACHE"] = "no" + +if uw.mpi.size == 1: + import pyvista as pv + import underworld3.visualisation as vis + +# mesh options +res = 1/8 +res_int_fac = 1/2 +r_o = 2.0 +r_int = 1.8 +r_i = 1.0 + +# + +# visualize analytical solutions +plot_ana = False + +n = 2 # wave number + +# - + +# ### Analytical Solution + +if plot_ana: + solution_above = assess.CylindricalStokesSolutionDeltaFreeSlip(n, +1, Rp=r_o, Rm=r_i, rp=r_int, nu=1.0, g=-1.0) + solution_below = assess.CylindricalStokesSolutionDeltaFreeSlip(n, -1, Rp=r_o, Rm=r_i, rp=r_int, nu=1.0, g=-1.0) + +if plot_ana: + mesh_ana = uw.meshing.AnnulusInternalBoundary(radiusOuter=r_o, + radiusInternal=r_int, + radiusInner=r_i, + cellSize_Inner=res, + cellSize_Internal=res*res_int_fac, + cellSize_Outer=res,) + +if plot_ana: + v_ana = uw.discretisation.MeshVariable(r"\mathbf{u_a}", mesh_ana, 2, degree=2) + p_ana = uw.discretisation.MeshVariable(r"p_a", mesh_ana, 1, degree=1) + rho_ana = uw.discretisation.MeshVariable(r"rho_a", mesh_ana, 1, degree=1) + +if uw.mpi.size == 1 and plot_ana: + + pvmesh = vis.mesh_to_pv_mesh(mesh_ana) + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh(pvmesh, edge_color="Grey", show_edges=True, use_transparency=False, opacity=1.0, ) + + pl.show(cpos="xy") + +if plot_ana: + r_ana, th_ana = mesh_ana.CoordinateSystem.xR + +if plot_ana: + with mesh_ana.access(v_ana, p_ana, rho_ana): + # velocities + r = uw.function.evalf(r_ana, v_ana.coords) + for i, coord in enumerate(v_ana.coords): + if r[i]>r_int: + v_ana.data[i] = solution_above.velocity_cartesian(coord) + else: + v_ana.data[i] = solution_below.velocity_cartesian(coord) + + + # pressure + r = uw.function.evalf(r_ana, p_ana.coords) + for i, coord in enumerate(p_ana.coords): + if r[i]>r_int: + p_ana.data[i] = solution_above.pressure_cartesian(coord) + else: + p_ana.data[i] = solution_below.pressure_cartesian(coord) + + # density + r = uw.function.evalf(r_ana, rho_ana.coords) + for i, coord in enumerate(rho_ana.coords): + if r[i]>r_int: + rho_ana.data[i] = solution_above.radial_stress_cartesian(coord) + else: + rho_ana.data[i] = solution_below.radial_stress_cartesian(coord) + +if uw.mpi.size == 1 and plot_ana: + pvmesh_ana = vis.mesh_to_pv_mesh(mesh_ana) + pvmesh_ana.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh_ana, v_ana.sym) + pvmesh_ana.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh_ana, + sympy.sqrt(v_ana.sym.dot(v_ana.sym))) + + print(pvmesh_ana.point_data["Vmag"].min(), pvmesh_ana.point_data["Vmag"].max()) + + velocity_points_ana = vis.meshVariable_to_pv_cloud(v_ana) + velocity_points_ana.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points_ana, v_ana.sym) + + pl = pv.Plotter(window_size=(750, 750)) + pl.add_mesh(pvmesh_ana, cmap=cmc.lapaz.resampled(11), edge_color="Grey", + scalars="Vmag", show_edges=False, use_transparency=False, + opacity=0.7, clim=[0., 0.05] ) + pl.add_arrows(velocity_points_ana.points[::5], velocity_points_ana.point_data["V"][::5], + mag=5e0, color='k') + + pl.show(cpos="xy") + +if uw.mpi.size == 1 and plot_ana: + pvmesh_ana = vis.mesh_to_pv_mesh(mesh_ana) + pvmesh_ana.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh_ana, p_ana.sym) + + print(pvmesh_ana.point_data["P"].min(), pvmesh_ana.point_data["P"].max()) + + pl = pv.Plotter(window_size=(750, 750)) + pl.add_mesh(pvmesh_ana, cmap=cmc.vik.resampled(41), edge_color="Grey", + scalars="P", show_edges=False, use_transparency=False, + opacity=1.0, clim=[-0.65, 0.65] ) + + pl.show(cpos="xy") + +# + +# This one is actually tau_rr + +if uw.mpi.size == 1 and plot_ana: + pvmesh_ana = vis.mesh_to_pv_mesh(mesh_ana) + pvmesh_ana.point_data["rho"] = vis.scalar_fn_to_pv_points(pvmesh_ana, rho_ana.sym) + + print(pvmesh_ana.point_data["rho"].min(), pvmesh_ana.point_data["rho"].max()) + + pl = pv.Plotter(window_size=(750, 750)) + pl.add_mesh(pvmesh_ana, cmap=cmc.roma.resampled(41), edge_color="Grey", + scalars="rho", show_edges=False, use_transparency=False, + opacity=1.0) # , clim=[-0.8, 0.8] ) + + pl.show(cpos="xy") +# - + +# ### Create Mesh for Numerical Solution + +# mesh +mesh_uw = uw.meshing.AnnulusInternalBoundary(radiusOuter=r_o, + radiusInternal=r_int, + radiusInner=r_i, + cellSize_Inner=res, + cellSize_Internal=res*res_int_fac, + cellSize_Outer=res,) + +# mesh variables +v_uw = uw.discretisation.MeshVariable(r"\mathbf{u}", mesh_uw, 2, degree=2) +p_uw = uw.discretisation.MeshVariable(r"p", mesh_uw, 1, degree=1, continuous=True) +v_err = uw.discretisation.MeshVariable(r"\mathbf{u_e}", mesh_uw, 2, degree=2) +p_err = uw.discretisation.MeshVariable(r"p_e", mesh_uw, 1, degree=1, continuous=True) + +# Some useful coordinate stuff +unit_rvec = mesh_uw.CoordinateSystem.unit_e_0 +r_uw, th_uw = mesh_uw.CoordinateSystem.xR + +# + +# Create Stokes object + +stokes = Stokes(mesh_uw, velocityField=v_uw, pressureField=p_uw, solver_name="stokes") + +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.viscosity = 1.0 +stokes.saddle_preconditioner = 1.0 + +rho = sympy.cos(n*th_uw) + +Gamma = unit_rvec +Gamma = mesh_uw.Gamma +penalty = 1e6 + +stokes.add_natural_bc(penalty * Gamma.dot(v_uw.sym) * Gamma, "Upper") +stokes.add_natural_bc(penalty * Gamma.dot(v_uw.sym) * Gamma, "Lower") +stokes.add_natural_bc(-rho * unit_rvec, "Internal") + +stokes.bodyforce = sympy.Matrix([0,0]) +# - + +mesh_uw.dm.view() + +if uw.mpi.size == 1: + pvmesh = vis.mesh_to_pv_mesh(mesh_uw) + pvmesh.point_data["rho"] = vis.scalar_fn_to_pv_points(pvmesh, rho * sympy.exp(-1e5 * ((r_uw - r_int) ** 2))) + + print(pvmesh.point_data["rho"].min(), pvmesh.point_data["rho"].max()) + + pl = pv.Plotter(window_size=(750, 750)) + pl.add_mesh(pvmesh, cmap=cmc.roma.resampled(31), edge_color="Grey", + scalars="rho", show_edges=False, use_transparency=False, + opacity=1.0, ) # clim=[-1, 1] ) + + pl.show(cpos="xy") + +# + +# Stokes settings + +stokes.tolerance = 1.0e-5 +stokes.petsc_options["ksp_monitor"] = None + +stokes.petsc_options["snes_type"] = "newtonls" +stokes.petsc_options["ksp_type"] = "fgmres" + +# stokes.petsc_options.setValue("fieldsplit_velocity_pc_type", "mg") +# stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_type", "kaskade") +# stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_cycle_type", "w") + +stokes.petsc_options["fieldsplit_velocity_mg_coarse_pc_type"] = "svd" + +# stokes.petsc_options[f"fieldsplit_velocity_ksp_type"] = "fcg" +# stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_type"] = "chebyshev" +# stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_max_it"] = 5 +# stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_converged_maxits"] = None + +# gasm is super-fast ... but mg seems to be bulletproof +# gamg is toughest wrt viscosity + +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "gamg") +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "additive") +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v") + +# # # mg, multiplicative - very robust ... similar to gamg, additive + +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "mg") +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "multiplicative") +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v") +# - + +stokes._setup_pointwise_functions() +stokes._setup_discretisation() +stokes._setup_solver() + +stokes.solve(verbose=True) + +if uw.mpi.size == 1 and plot_ana: + + with mesh_uw.access(v_uw, p_uw, v_err, p_err): + # velocities + r = uw.function.evalf(r_uw, v_err.coords) + for i, coord in enumerate(v_err.coords): + if r[i]>r_int: + v_err.data[i] = v_uw.data[i] - solution_above.velocity_cartesian(coord) + else: + v_err.data[i] = v_uw.data[i] - solution_below.velocity_cartesian(coord) + + + # pressure + r = uw.function.evalf(r_uw, p_err.coords) + for i, coord in enumerate(p_err.coords): + if r[i]>r_int: + p_err.data[i] = p_uw.data[i] - solution_above.pressure_cartesian(coord) + else: + p_err.data[i] = p_uw.data[i] - solution_below.pressure_cartesian(coord) + +# plotting velocities from uw +if uw.mpi.size == 1: + pvmesh = vis.mesh_to_pv_mesh(mesh_uw) + pvmesh.point_data["V_uw"] = vis.vector_fn_to_pv_points(pvmesh, v_uw.sym) + pvmesh.point_data["Vmag_uw"] = vis.scalar_fn_to_pv_points(pvmesh, sympy.sqrt(v_uw.sym.dot(v_uw.sym))) + + print(pvmesh.point_data["Vmag_uw"].min(), pvmesh.point_data["Vmag_uw"].max()) + + velocity_points = vis.meshVariable_to_pv_cloud(v_uw) + velocity_points.point_data["V_uw"] = vis.vector_fn_to_pv_points(velocity_points, v_uw.sym) + + pl = pv.Plotter(window_size=(750, 750)) + pl.add_mesh(pvmesh, cmap=cmc.lapaz.resampled(11), edge_color="Grey", + scalars="Vmag_uw", show_edges=False, use_transparency=False, + opacity=0.7,)# clim=[0., 0.05] ) + pl.add_arrows(velocity_points.points[::10], velocity_points.point_data["V_uw"][::10], mag=10, color='k') + + pl.show(cpos="xy") + +# plotting error in velocities +if uw.mpi.size == 1: + pvmesh = vis.mesh_to_pv_mesh(mesh_uw) + + pvmesh.point_data["V_err"] = vis.vector_fn_to_pv_points(pvmesh, v_err.sym) + pvmesh.point_data["Vmag_err"] = vis.scalar_fn_to_pv_points(pvmesh, sympy.sqrt(v_err.sym.dot(v_err.sym))) + + print(pvmesh.point_data["Vmag_err"].min(), pvmesh.point_data["Vmag_err"].max()) + + velocity_points = vis.meshVariable_to_pv_cloud(v_uw) + velocity_points.point_data["V_err"] = vis.vector_fn_to_pv_points(velocity_points, v_err.sym) + + pl = pv.Plotter(window_size=(750, 750)) + pl.add_mesh(pvmesh, cmap=cmc.lapaz.resampled(11), edge_color="Grey", + scalars="Vmag_err", show_edges=False, use_transparency=False, + opacity=0.7, clim=[0., 0.005] ) + pl.add_arrows(velocity_points.points[::50], velocity_points.point_data["V_err"][::50], mag=100, color='k') + + pl.show(cpos="xy") + +# plotting pressure from uw +if uw.mpi.size == 1: + pvmesh = vis.mesh_to_pv_mesh(mesh_uw) + pvmesh.point_data["P_uw"] = vis.scalar_fn_to_pv_points(pvmesh, p_uw.sym) + + print(pvmesh.point_data["P_uw"].min(), pvmesh.point_data["P_uw"].max()) + + pl = pv.Plotter(window_size=(750, 750)) + pl.add_mesh(pvmesh, cmap=cmc.vik.resampled(41), edge_color="Grey", + scalars="P_uw", show_edges=False, use_transparency=False, + opacity=1.0, clim=[-0.85, 0.85] ) + + pl.show(cpos="xy") + +# plotting error in uw +if uw.mpi.size == 1: + pvmesh = vis.mesh_to_pv_mesh(mesh_uw) + pvmesh.point_data["P_err"] = vis.scalar_fn_to_pv_points(pvmesh, p_err.sym) + + print(pvmesh.point_data["P_err"].min(), pvmesh.point_data["P_err"].max()) + + pl = pv.Plotter(window_size=(750, 750)) + pl.add_mesh(pvmesh, cmap=cmc.vik.resampled(41), edge_color="Grey", + scalars="P_err", show_edges=False, use_transparency=False, + opacity=1.0, clim=[-0.085, 0.085] ) + + pl.show(cpos="xy") + +# pressure error (L2 norm) +p_err.stats()[5]/p_ana.stats()[5] + +mesh_uw.dm.view() + + diff --git a/main/_sources/Notebooks/Examples-Utilities/Ex_Stokes_Annulus_Benchmark_Thieulot.py b/main/_sources/Notebooks/Examples-Utilities/Ex_Stokes_Annulus_Benchmark_Thieulot.py new file mode 100644 index 0000000..456bc91 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Utilities/Ex_Stokes_Annulus_Benchmark_Thieulot.py @@ -0,0 +1,328 @@ +# ## Annulus Benchmark: Isoviscous Incompressible Stokes +# +# ### Case: Infinitely thin density anomaly at $r = r'$ +# #### [Benchmark paper](https://gmd.copernicus.org/articles/14/1899/2021/) +# +# *Author: [Thyagarajulu Gollapalli](https://github.com/gthyagi)* + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import underworld3 as uw +from underworld3.systems import Stokes + +import numpy as np +import sympy +from sympy import lambdify +import os +import matplotlib.pyplot as plt +import cmcrameri.cm as cmc +import assess +# - + +os.environ["SYMPY_USE_CACHE"] = "no" + +if uw.mpi.size == 1: + import pyvista as pv + import underworld3.visualisation as vis + +# mesh options +res = 1/32 +res_int_fac = 1/2 +r_o = 2.0 +r_int = 1.8 +r_i = 1.0 + +# visualize analytical solutions +plot_ana = True + +# ### Analytical Solution + +if plot_ana: + n = 2 # wave number + solution_above = assess.CylindricalStokesSolutionDeltaFreeSlip(n, +1, Rp=r_o, Rm=r_i, rp=r_int, nu=1.0, g=-1.0) + solution_below = assess.CylindricalStokesSolutionDeltaFreeSlip(n, -1, Rp=r_o, Rm=r_i, rp=r_int, nu=1.0, g=-1.0) + +if plot_ana: + mesh_ana = uw.meshing.AnnulusInternalBoundary(radiusOuter=r_o, + radiusInternal=r_int, + radiusInner=r_i, + cellSize_Inner=res, + cellSize_Internal=res*res_int_fac, + cellSize_Outer=res,) + +if plot_ana: + v_ana = uw.discretisation.MeshVariable(r"\mathbf{u_a}", mesh_ana, 2, degree=2) + p_ana = uw.discretisation.MeshVariable(r"p_a", mesh_ana, 1, degree=1) + rho_ana = uw.discretisation.MeshVariable(r"rho_a", mesh_ana, 1, degree=1) + +if uw.mpi.size == 1 and plot_ana: + + pvmesh = vis.mesh_to_pv_mesh(mesh_ana) + + pl = pv.Plotter(window_size=(750, 750)) + + pl.add_mesh(pvmesh, edge_color="Grey", show_edges=True, use_transparency=False, opacity=1.0, ) + + pl.show(cpos="xy") + +if plot_ana: + r_ana, th_ana = mesh_ana.CoordinateSystem.xR + +if plot_ana: + with mesh_ana.access(v_ana, p_ana, rho_ana): + # velocities + r = uw.function.evalf(r_ana, v_ana.coords) + for i, coord in enumerate(v_ana.coords): + if r[i]>r_int: + v_ana.data[i] = solution_above.velocity_cartesian(coord) + else: + v_ana.data[i] = solution_below.velocity_cartesian(coord) + + + # pressure + r = uw.function.evalf(r_ana, p_ana.coords) + for i, coord in enumerate(p_ana.coords): + if r[i]>r_int: + p_ana.data[i] = solution_above.pressure_cartesian(coord) + else: + p_ana.data[i] = solution_below.pressure_cartesian(coord) + + # density + r = uw.function.evalf(r_ana, rho_ana.coords) + for i, coord in enumerate(rho_ana.coords): + if r[i]>r_int: + rho_ana.data[i] = solution_above.radial_stress_cartesian(coord) + else: + rho_ana.data[i] = solution_below.radial_stress_cartesian(coord) + +if uw.mpi.size == 1 and plot_ana: + pvmesh_ana = vis.mesh_to_pv_mesh(mesh_ana) + pvmesh_ana.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh_ana, v_ana.sym) + pvmesh_ana.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh_ana, + sympy.sqrt(v_ana.sym.dot(v_ana.sym))) + + print(pvmesh_ana.point_data["Vmag"].min(), pvmesh_ana.point_data["Vmag"].max()) + + velocity_points_ana = vis.meshVariable_to_pv_cloud(v_ana) + velocity_points_ana.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points_ana, v_ana.sym) + + pl = pv.Plotter(window_size=(750, 750)) + pl.add_mesh(pvmesh_ana, cmap=cmc.lapaz.resampled(11), edge_color="Grey", + scalars="Vmag", show_edges=False, use_transparency=False, + opacity=0.7, clim=[0., 0.05] ) + pl.add_arrows(velocity_points_ana.points[::10], velocity_points_ana.point_data["V"][::10], + mag=5, color='k') + + pl.show(cpos="xy") + +if uw.mpi.size == 1 and plot_ana: + pvmesh_ana = vis.mesh_to_pv_mesh(mesh_ana) + pvmesh_ana.point_data["P"] = vis.scalar_fn_to_pv_points(pvmesh_ana, p_ana.sym) + + print(pvmesh_ana.point_data["P"].min(), pvmesh_ana.point_data["P"].max()) + + pl = pv.Plotter(window_size=(750, 750)) + pl.add_mesh(pvmesh_ana, cmap=cmc.vik.resampled(41), edge_color="Grey", + scalars="P", show_edges=False, use_transparency=False, + opacity=1.0, clim=[-0.65, 0.65] ) + + pl.show(cpos="xy") + +if uw.mpi.size == 1 and plot_ana: + pvmesh_ana = vis.mesh_to_pv_mesh(mesh_ana) + pvmesh_ana.point_data["rho"] = vis.scalar_fn_to_pv_points(pvmesh_ana, rho_ana.sym) + + print(pvmesh_ana.point_data["rho"].min(), pvmesh_ana.point_data["rho"].max()) + + pl = pv.Plotter(window_size=(750, 750)) + pl.add_mesh(pvmesh_ana, cmap=cmc.roma.resampled(41), edge_color="Grey", + scalars="rho", show_edges=False, use_transparency=False, + opacity=1.0, clim=[-0.8, 0.8] ) + + pl.show(cpos="xy") + +# ### Create Mesh for Numerical Solution + +# mesh +mesh_uw = uw.meshing.AnnulusInternalBoundary(radiusOuter=r_o, + radiusInternal=r_int, + radiusInner=r_i, + cellSize_Inner=res, + cellSize_Internal=res*res_int_fac, + cellSize_Outer=res,) + +# mesh variables +v_uw = uw.discretisation.MeshVariable(r"\mathbf{u}", mesh_uw, 2, degree=2) +p_uw = uw.discretisation.MeshVariable(r"p", mesh_uw, 1, degree=1) +v_err = uw.discretisation.MeshVariable(r"\mathbf{u_e}", mesh_uw, 2, degree=2) +p_err = uw.discretisation.MeshVariable(r"p_e", mesh_uw, 1, degree=1) + +# Some useful coordinate stuff +unit_rvec = mesh_uw.CoordinateSystem.unit_e_0 +r_uw, th_uw = mesh_uw.CoordinateSystem.xR + +# + +# Create Stokes object + +stokes = Stokes(mesh_uw, velocityField=v_uw, pressureField=p_uw, solver_name="stokes") + +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.viscosity = 1.0 +stokes.saddle_preconditioner = 1.0 + +rho = sympy.cos(n*th_uw) * sympy.exp(-1e5 * ((r_uw - r_int) ** 2)) + +penalty = 1e7 +Gamma = mesh_uw.Gamma +stokes.add_natural_bc(penalty * Gamma.dot(v_uw.sym) * Gamma, "Upper") +stokes.add_natural_bc(penalty * Gamma.dot(v_uw.sym) * Gamma, "Lower") +stokes.add_natural_bc(-rho * unit_rvec, "Internal") + +stokes.bodyforce = sympy.Matrix([0,0]) +# - + +if uw.mpi.size == 1: + pvmesh = vis.mesh_to_pv_mesh(mesh_uw) + pvmesh.point_data["rho"] = vis.scalar_fn_to_pv_points(pvmesh, rho) + + print(pvmesh.point_data["rho"].min(), pvmesh.point_data["rho"].max()) + + pl = pv.Plotter(window_size=(750, 750)) + pl.add_mesh(pvmesh, cmap=cmc.roma.resampled(31), edge_color="Grey", + scalars="rho", show_edges=False, use_transparency=False, + opacity=1.0, clim=[-1, 1] ) + + pl.show(cpos="xy") + +# + +# Stokes settings + +stokes.tolerance = 1.0e-6 +stokes.petsc_options["ksp_monitor"] = None + +stokes.petsc_options["snes_type"] = "newtonls" +stokes.petsc_options["ksp_type"] = "fgmres" + +# stokes.petsc_options.setValue("fieldsplit_velocity_pc_type", "mg") +stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_type", "kaskade") +stokes.petsc_options.setValue("fieldsplit_velocity_pc_mg_cycle_type", "w") + +stokes.petsc_options["fieldsplit_velocity_mg_coarse_pc_type"] = "svd" + +stokes.petsc_options[f"fieldsplit_velocity_ksp_type"] = "fcg" +stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_type"] = "chebyshev" +stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_max_it"] = 5 +stokes.petsc_options[f"fieldsplit_velocity_mg_levels_ksp_converged_maxits"] = None + +# gasm is super-fast ... but mg seems to be bulletproof +# gamg is toughest wrt viscosity + +stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "gamg") +stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "additive") +stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v") + +# # # mg, multiplicative - very robust ... similar to gamg, additive + +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_type", "mg") +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_type", "multiplicative") +# stokes.petsc_options.setValue("fieldsplit_pressure_pc_mg_cycle_type", "v") +# - + +stokes.solve() + +with mesh_uw.access(v_uw, p_uw, v_err, p_err): + # velocities + r = uw.function.evalf(r_uw, v_err.coords) + for i, coord in enumerate(v_err.coords): + if r[i]>r_int: + v_err.data[i] = v_uw.data[i] - solution_above.velocity_cartesian(coord) + else: + v_err.data[i] = v_uw.data[i] - solution_below.velocity_cartesian(coord) + + + # pressure + r = uw.function.evalf(r_uw, p_err.coords) + for i, coord in enumerate(p_err.coords): + if r[i]>r_int: + p_err.data[i] = p_uw.data[i] - solution_above.pressure_cartesian(coord) + else: + p_err.data[i] = p_uw.data[i] - solution_below.pressure_cartesian(coord) + +# plotting velocities from uw +if uw.mpi.size == 1: + pvmesh = vis.mesh_to_pv_mesh(mesh_uw) + pvmesh.point_data["V_uw"] = vis.vector_fn_to_pv_points(pvmesh, v_uw.sym) + pvmesh.point_data["Vmag_uw"] = vis.scalar_fn_to_pv_points(pvmesh, sympy.sqrt(v_uw.sym.dot(v_uw.sym))) + + print(pvmesh.point_data["Vmag_uw"].min(), pvmesh.point_data["Vmag_uw"].max()) + + velocity_points = vis.meshVariable_to_pv_cloud(v_uw) + velocity_points.point_data["V_uw"] = vis.vector_fn_to_pv_points(velocity_points, v_uw.sym) + + pl = pv.Plotter(window_size=(750, 750)) + pl.add_mesh(pvmesh, cmap=cmc.lapaz.resampled(11), edge_color="Grey", + scalars="Vmag_uw", show_edges=False, use_transparency=False, + opacity=0.1, clim=[0., 0.05] ) + pl.add_arrows(velocity_points.points[::10], velocity_points.point_data["V_uw"][::10], mag=1e1, color='k') + + pl.show(cpos="xy") + +# plotting errror in velocities +if uw.mpi.size == 1: + pvmesh = vis.mesh_to_pv_mesh(mesh_uw) + + pvmesh.point_data["V_err"] = vis.vector_fn_to_pv_points(pvmesh, v_err.sym) + pvmesh.point_data["Vmag_err"] = vis.scalar_fn_to_pv_points(pvmesh, sympy.sqrt(v_err.sym.dot(v_err.sym))) + + print(pvmesh.point_data["Vmag_err"].min(), pvmesh.point_data["Vmag_err"].max()) + + velocity_points = vis.meshVariable_to_pv_cloud(v_uw) + velocity_points.point_data["V_err"] = vis.vector_fn_to_pv_points(velocity_points, v_err.sym) + + pl = pv.Plotter(window_size=(750, 750)) + pl.add_mesh(pvmesh, cmap=cmc.lapaz.resampled(11), edge_color="Grey", + scalars="Vmag_err", show_edges=False, use_transparency=False, + opacity=0.7, clim=[0., 0.005] ) + pl.add_arrows(velocity_points.points[::50], velocity_points.point_data["V_err"][::50], mag=1e-1, color='k') + + pl.show(cpos="xy") + +# plotting pressure from uw +if uw.mpi.size == 1: + pvmesh = vis.mesh_to_pv_mesh(mesh_uw) + pvmesh.point_data["P_uw"] = vis.scalar_fn_to_pv_points(pvmesh, p_uw.sym) + + print(pvmesh.point_data["P_uw"].min(), pvmesh.point_data["P_uw"].max()) + + pl = pv.Plotter(window_size=(750, 750)) + pl.add_mesh(pvmesh, cmap=cmc.vik.resampled(41), edge_color="Grey", + scalars="P_uw", show_edges=False, use_transparency=False, + opacity=1.0, clim=[-0.85, 0.85] ) + + pl.show(cpos="xy") + +# plotting error in uw +if uw.mpi.size == 1: + pvmesh = vis.mesh_to_pv_mesh(mesh_uw) + pvmesh.point_data["P_err"] = vis.scalar_fn_to_pv_points(pvmesh, p_err.sym) + + print(pvmesh.point_data["P_err"].min(), pvmesh.point_data["P_err"].max()) + + pl = pv.Plotter(window_size=(750, 750)) + pl.add_mesh(pvmesh, cmap=cmc.vik.resampled(41), edge_color="Grey", + scalars="P_err", show_edges=False, use_transparency=False, + opacity=1.0, clim=[-0.085, 0.085] ) + + pl.show(cpos="xy") + +# pressure error (L2 norm) +p_err.stats()[5]/p_ana.stats()[5] + +mesh_uw.dm.view() + + + + diff --git a/main/_sources/Notebooks/Examples-Utilities/Ex_SurfaceIntegrals_BCs.py b/main/_sources/Notebooks/Examples-Utilities/Ex_SurfaceIntegrals_BCs.py new file mode 100644 index 0000000..46d5100 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Utilities/Ex_SurfaceIntegrals_BCs.py @@ -0,0 +1,291 @@ +# ## Surface integral / natural boundary conditions +# +# + +# + +import underworld3 as uw +import numpy as np +import sympy +import os +import sys +import petsc4py +import matplotlib.pyplot as plt + +import nest_asyncio +nest_asyncio.apply() + +# + +minX = -1.0 +maxX = 1.0 +minY = -1.0 +maxY = 1.0 + +resX = 4 +resY = 4 + +cell_height = maxY / resY +cell_width = maxX / resX + +meshQuad = uw.meshing.StructuredQuadBox( + elementRes=(resX, resY), + minCoords=(minX, minY), + maxCoords=(maxX, maxY), qdegree=3) + +meshTri = uw.meshing.UnstructuredSimplexBox( + regular=True, + cellSize=1/2, + minCoords=(minX, minY), + maxCoords=(maxX, maxY), qdegree=3) + +mesh = meshTri +x,y = mesh.X + +# - + +uw.systems.Stokes.view() + +# + + +stokes0 = uw.systems.Stokes(mesh, solver_name="Stokes0") +stokes0.constitutive_model = uw.constitutive_models.ViscousFlowModel + +v0 = stokes0.Unknowns.u +p0 = stokes0.Unknowns.p + +stokes0.add_essential_bc( [0.,0.], "Bottom") # no slip on the base +stokes0.add_essential_bc( [0.,sympy.oo], "Left") # free slip Left/Right +stokes0.add_essential_bc( [0.,sympy.oo], "Right") # no slip on the top +stokes0.bodyforce = sympy.Matrix([0, sympy.sin(x*sympy.pi)]) + +### see the SNES output +stokes0.petsc_options["snes_converged_reason"] = None +stokes0.petsc_options["snes_monitor_short"] = None +stokes0.tolerance = 1.0e-6 + +stokes0.solve() + + + +# + +def JacViewer(stokes_solver): + + Jac, JacP, _ = stokes_solver.snes.getJacobian() + ii,jj = Jac.getSize() + + ## Let's take a look (Sorry, Matt, using python for this) + + Jm = Jac.getValues(range(0,ii),range(0,jj)) + JPm = JacP.getValues(range(0,ii),range(0,jj)) + + fig = plt.figure(figsize=(12,6), facecolor="none") + ax = plt.subplot(131) # 2x1 array of plots, ax refers to the 1st of them + ax2 = plt.subplot(132) # 1x1 array of plots, ax refers to the 1st of them + ax3 = plt.subplot(133) # 1x1 array of plots, ax refers to the 1st of them + + ax.imshow(Jm, vmin=-1, vmax=1, cmap="coolwarm") + ax2.imshow(JPm, vmin=-1, vmax=1, cmap="coolwarm") + ax3.imshow(JPm-Jm, vmin=-0.01, vmax=0.01, cmap="coolwarm") + +def JacDiffViewer(stokes_solver1, stokes_solver2): + + Jac1, JacP1, _ = stokes_solver1.snes.getJacobian() + ii,jj = Jac1.getSize() + + Jac2, JacP2, _ = stokes_solver2.snes.getJacobian() + + + ## Let's take a look (Sorry, Matt, using python for this) + + Jm1 = Jac1.getValues(range(0,ii),range(0,jj)) + JPm1 = JacP1.getValues(range(0,ii),range(0,jj)) + + Jm2 = Jac2.getValues(range(0,ii),range(0,jj)) + JPm2 = JacP2.getValues(range(0,ii),range(0,jj)) + + fig = plt.figure(figsize=(12,6), facecolor="none") + ax = plt.subplot(121) # 2x1 array of plots, ax refers to the 1st of them + ax2 = plt.subplot(122) # 1x1 array of plots, ax refers to the 1st of them + # ax3 = plt.subplot(133) # 1x1 array of plots, ax refers to the 1st of them + + JmaxD = np.abs(Jm1-Jm2).max() + JPmaxD = np.abs(JPm1-JPm2).max() + + print(f"Jac: min {(Jm1-Jm2).min()} / max {(Jm1-Jm2).max()}", flush=True) + print(f"JacP: min {(JPm1-JPm2).min()} / max {(JPm1-JPm2).max()}", flush=True) + + ax.imshow(Jm1-Jm2, vmin=-JmaxD, vmax=JmaxD, cmap="coolwarm") + ax2.imshow(JPm1-JPm2, vmin=-JPmaxD, vmax=JPmaxD, cmap="coolwarm") + +JacViewer(stokes0) + + + +# + +## Now try adding a null natural bc - Expect no changes to J, P though +## the surface terms should be active if we check the debug output + +stokes1 = uw.systems.Stokes(mesh, solver_name="Stokes1") +stokes1.constitutive_model = uw.constitutive_models.ViscousFlowModel + +v1 = stokes1.Unknowns.u +p1 = stokes1.Unknowns.p + +stokes1.petsc_options["snes_converged_reason"] = None +stokes1.petsc_options["snes_monitor_short"] = None +stokes1.tolerance = 1.0e-6 + +stokes1.add_essential_bc( [0.,0.], "Bottom") # no slip on the base +stokes1.add_essential_bc( [0.,sympy.oo], "Left") # free slip Left/Right +stokes1.add_essential_bc( [0.,sympy.oo], "Right") # free slip Left/Right +stokes1.add_natural_bc( [0.,0.], "Top") # Top is open (still) + +stokes1.bodyforce = sympy.Matrix([0, sympy.sin(x*sympy.pi)]) + +# - + + +stokes1._setup_pointwise_functions() +stokes1._setup_discretisation() +stokes1._setup_problem_description() + +# + + +stokes1.solve(verbose=False, debug=False, zero_init_guess=True, picard=0, _force_setup=False) +# - + + +JacViewer(stokes1) +JacDiffViewer(stokes0, stokes1) +# + +## Now try adding a constant natural bc - Still expect no changes to J, P though +## the surface terms should be active if we check the debug output + +stokes2 = uw.systems.Stokes(mesh, solver_name="Stokes2") +stokes2.constitutive_model = uw.constitutive_models.ViscousFlowModel + +v2 = stokes2.Unknowns.u +p2 = stokes2.Unknowns.p + +stokes2.petsc_options["snes_converged_reason"] = None +stokes2.petsc_options["snes_monitor_short"] = None +stokes2.tolerance = 1.0e-6 + +stokes2.add_essential_bc( [0.,0.], "Bottom") # no slip on the base +stokes2.add_essential_bc( [0.,sympy.oo], "Left") # free slip Left/Right +stokes2.add_essential_bc( [0.,sympy.oo], "Right") # free slip Left/Right +stokes2.add_natural_bc( [1.,0.], "Top") # Top is driven (constant) + +stokes2.bodyforce = sympy.Matrix([0, sympy.sin(x*sympy.pi)]) + +stokes2.solve(verbose=False, debug=False, zero_init_guess=True, picard=5, _force_setup=False) + +# - + +JacViewer(stokes2) +JacDiffViewer(stokes0, stokes2) + +# + +## Now try adding a constant natural bc. J, P should be unchanged as this is +## linear, but we should see a different RHS reflected in the SNES norms + +stokes3 = uw.systems.Stokes(mesh, solver_name="Stokes3") +stokes3.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes3.petsc_options.setValue("snes_monitor", None) + +v3 = stokes3.Unknowns.u +p3 = stokes3.Unknowns.p + +stokes3.add_essential_bc( [0.,0.], "Bottom") # no slip on the base +stokes3.add_essential_bc( [0.,sympy.oo], "Left") # free slip Left/Right +stokes3.add_essential_bc( [0.,sympy.oo], "Right") # free slip Left/Right +# stokes3.add_natural_bc( [0.,1000000*v3.sym[1]], "Top") # Top "free slip / penalty" + +Gamma = mesh.Gamma # sympy.Piecewise((mesh.Gamma, x < 0.5), mesh.CoordinateSystem.unit_j +Gamma = sympy.Matrix([0,1]) +stokes3.add_natural_bc( 1.0e8 * Gamma.dot(v3.sym) * Gamma, "Top") # Top "free slip / penalty" + +stokes3.bodyforce = sympy.Matrix([0, sympy.sin(x*sympy.pi)]) + +# Set verbose / debug to True to see the functions being executed +stokes3.solve(verbose=False, debug=False, zero_init_guess=True, picard=2) + +# - + + +JacViewer(stokes3) +JacDiffViewer(stokes3, stokes0) + + +# + +## Validation step - Did the penalty approach actually work + +stokesN = uw.systems.Stokes(mesh, solver_name="StokesN") +stokesN.constitutive_model = uw.constitutive_models.ViscousFlowModel +vN = stokesN.Unknowns.u + +stokesN.tolerance = 1.0e-6 +stokesN.petsc_options.setValue("snes_monitor", None) +stokesN.petsc_options.setValue("ksp_monitor", None) + +stokesN.add_essential_bc( [0.,0.], "Bottom") # no slip on the base +stokesN.add_essential_bc( [0.,sympy.oo], "Left") # free slip Left/Right +stokesN.add_essential_bc( [0.,sympy.oo], "Right") # free slip Left/Right +stokesN.add_essential_bc( [sympy.oo, 0.], "Top") # Top "free slip / penalty" + + +stokesN.bodyforce = sympy.Matrix([0, sympy.sin(x*sympy.pi)]) + +stokesN.solve(verbose=False, debug=False, zero_init_guess=True) + + +# + +# Visuals + +# This creates a plot of the true free-surface solution, the penalized velocity solution, +# and their difference + + +import underworld3 as uw +import pyvista as pv +import underworld3.visualisation + +pl = pv.Plotter(window_size=(1000, 500)) + +pvmesh = uw.visualisation.mesh_to_pv_mesh(mesh) +pvmesh.point_data["V0"] = uw.visualisation.vector_fn_to_pv_points(pvmesh, vN.sym) +pvmesh.point_data["V3"] = uw.visualisation.vector_fn_to_pv_points(pvmesh, v3.sym) +pvmesh.point_data["P"] = uw.visualisation.scalar_fn_to_pv_points(pvmesh, p3.sym) +# pvmesh.point_data["Vmag"] = uw.visualisation.scalar_fn_to_pv_points(pvmesh, v.sym.dot(v.sym)) + +velocity_points = underworld3.visualisation.meshVariable_to_pv_cloud(v0) +velocity_points.point_data["V0"] = uw.visualisation.vector_fn_to_pv_points(velocity_points, vN.sym) +velocity_points.point_data["V3"] = uw.visualisation.vector_fn_to_pv_points(velocity_points, v3.sym) + +pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="P", + use_transparency=False, + opacity=1.0, +) + + +pl.add_arrows(velocity_points.points, velocity_points.point_data["V3"]-velocity_points.point_data["V0"], mag=100000, opacity=0.75) +pl.add_arrows(velocity_points.points+(0.01,0.0,0.0), velocity_points.point_data["V3"], mag=1.0, opacity=0.75) +pl.add_arrows(velocity_points.points+(0.00,0.0,0.0), velocity_points.point_data["V0"], mag=1.0, opacity=0.75) +# pl.add_mesh(pvstream) + +pl.camera.SetPosition(0.75, 0.2, 1.5) +pl.camera.SetFocalPoint(0.75, 0.2, 0.0) +pl.camera.SetClippingRange(1.0, 8.0) + +# pl.remove_scalar_bar("Omega") +# pl.remove_scalar_bar("mag") +# pl.remove_scalar_bar("V") + +pl.show() + +# - +# # diff --git a/main/_sources/Notebooks/Examples-Utilities/Ex_Tensor_Variables_etc.py b/main/_sources/Notebooks/Examples-Utilities/Ex_Tensor_Variables_etc.py new file mode 100644 index 0000000..3b3159c --- /dev/null +++ b/main/_sources/Notebooks/Examples-Utilities/Ex_Tensor_Variables_etc.py @@ -0,0 +1,505 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# # Examples with General Mesh variable manipulation +# +# We introduce the notion of an `IndexSwarmVariable` which automatically generates masks for a swarm +# variable that consists of discrete level values (integers). +# +# For a variable $M$, the mask variables are $\left\{ M^0, M^1, M^2 \ldots M^N \right\}$ where $N$ is the number of indices (e.g. material types) on the variable. This value *must be defined in advance*. +# +# The masks are orthogonal in the sense that $M^i * M^j = 0$ if $i \ne j$, and they are complete in the sense that $\sum_i M^i = 1$ at all points. +# +# The masks are implemented as continuous mesh variables (the user can specify the interpolation order) and so they are also differentiable (once). +# + +# to fix trame issue +import nest_asyncio +nest_asyncio.apply() + +# + +import petsc4py +from petsc4py import PETSc + +import underworld3 as uw +from underworld3.systems import Stokes +from underworld3 import function + +import numpy as np +import sympy + +# - + +meshbox = uw.meshing.UnstructuredSimplexBox( + minCoords=(0.0, 0.0), maxCoords=(1.0, 1.0), cellSize=1.0 / 32.0 +) +meshbox.dm.view() + +# + +import sympy + +# Some useful coordinate stuff + +x, y = meshbox.CoordinateSystem.X +# - + + +v_soln = uw.discretisation.MeshVariable("U", meshbox, meshbox.dim, degree=2) +p_soln = uw.discretisation.MeshVariable("P", meshbox, 1, degree=1) + + +sympy.diff(v_soln.sym, x) + +swarm = uw.swarm.Swarm(mesh=meshbox) +material = uw.swarm.IndexSwarmVariable("M", swarm, indices=4) + + +swarm.populate(fill_param=5) + +with swarm.access(material): + material.data[...] = 0 + for i in range(50): + cx, cy, r = np.random.random(3) + m = np.random.randint(1, 4) + r = 0.025 + r * 0.025 + + inside = (swarm.data[:, 0] - cx) ** 2 + (swarm.data[:, 1] - cy) ** 2 < r**2 + material.data[inside] = m + + +material.sym.diff(meshbox.CoordinateSystem.X) + +material.sym.jacobian(meshbox.X).T + +meshbox.vector.jacobian(material.sym).T + +v_soln.sym.jacobian(meshbox.CoordinateSystem.X) + +mat_density = np.array([1, 10, 100, 1000]) +density = ( + mat_density[0] * material.sym[0] + + mat_density[1] * material.sym[1] + + mat_density[2] * material.sym[2] + + mat_density[3] * material.sym[3] +) + +mat_viscosity = np.array([1, 10, 100, 1000]) +viscosity = ( + mat_viscosity[0] * material.sym[0] + + mat_viscosity[1] * material.sym[1] + + mat_viscosity[2] * material.sym[2] + + mat_viscosity[3] * material.sym[3] +) + +# + +import numpy as np +import pyvista as pv +import vtk + +pv.global_theme.background = "white" +pv.global_theme.window_size = [750, 750] +pv.global_theme.antialiasing = True +pv.global_theme.jupyter_backend = "trame" +pv.global_theme.smooth_shading = True + + +meshbox.vtk("tmp_box.vtk") +pvmesh = pv.read("tmp_box.vtk") + +with swarm.access(): + points = np.zeros((swarm.data.shape[0], 3)) + points[:, 0] = swarm.data[:, 0] + points[:, 1] = swarm.data[:, 1] + points[:, 2] = 0.0 + +point_cloud = pv.PolyData(points) + +with meshbox.access(): + pvmesh.point_data["M0"] = uw.function.evaluate(material.sym[0], meshbox.data) + pvmesh.point_data["M1"] = uw.function.evaluate(material.sym[1], meshbox.data) + pvmesh.point_data["M2"] = uw.function.evaluate(material.sym[2], meshbox.data) + pvmesh.point_data["M3"] = uw.function.evaluate(material.sym[3], meshbox.data) + pvmesh.point_data["M"] = ( + 1.0 * pvmesh.point_data["M1"] + + 2.0 * pvmesh.point_data["M2"] + + 3.0 * pvmesh.point_data["M3"] + ) + + pvmesh.point_data["rho"] = uw.function.evaluate(density, meshbox.data) + pvmesh.point_data["visc"] = uw.function.evaluate(sympy.log(viscosity), meshbox.data) + + +with swarm.access(): + point_cloud.point_data["M"] = material.data.copy() + +pl = pv.Plotter(notebook=True) + +# pl.add_points(point_cloud, color="Black", +# render_points_as_spheres=False, +# point_size=2.5, opacity=0.75) + + +pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=False, + scalars="visc", + use_transparency=False, + opacity=0.95, +) + + +# pl.add_mesh(pvmesh, cmap="coolwarm", edge_color="Black", show_edges=True, scalars="M1", +# use_transparency=False, opacity=1.0) + + +pl.show(cpos="xy") + + +# + +ad = uw.systems.AdvDiffusionSwarm(meshbox, t_soln, T1.fn, degree=3, projection=True) + +ad._u_star_projector.smoothing = 0.0 + +ad.add_dirichlet_bc(1.0, "Bottom") +ad.add_dirichlet_bc(0.0, "Top") + +init_t = 0.01 * sympy.sin(5.0 * x) * sympy.sin(np.pi * y) + (1.0 - y) + +with meshbox.access(t_0, t_soln): + t_0.data[...] = uw.function.evaluate(init_t, t_0.coords).reshape(-1, 1) + t_soln.data[...] = t_0.data[...] + +with swarm.access(T1): + T1.data[...] = uw.function.evaluate( + init_t, swarm.particle_coordinates.data + ).reshape(-1, 1) + +# + +# Create Stokes object + +stokes = Stokes( + meshbox, + velocityField=v_soln, + pressureField=p_soln, + u_degree=v_soln.degree, + p_degree=p_soln.degree, + solver_name="stokes", + verbose=False, +) + +# Set solve options here (or remove default values +# stokes.petsc_options.getAll() +stokes.petsc_options.delValue("ksp_monitor") + +# Constant visc +stokes.viscosity = 1.0 + +# Velocity boundary conditions +stokes.add_dirichlet_bc((0.0,), "Left", (0,)) +stokes.add_dirichlet_bc((0.0,), "Right", (0,)) +stokes.add_dirichlet_bc((0.0,), "Top", (1,)) +stokes.add_dirichlet_bc((0.0,), "Bottom", (1,)) + + +# + +buoyancy_force = 1.0e6 * t_soln.fn +stokes.bodyforce = meshbox.N.j * buoyancy_force + +# check the stokes solve is set up and that it converges +stokes.solve() + + +# + +# check the projection + + +if uw.mpi.size == 1 and ad.projection: + + import numpy as np + import pyvista as pv + import vtk + + pv.global_theme.background = "white" + pv.global_theme.window_size = [750, 250] + pv.global_theme.antialiasing = True + pv.global_theme.jupyter_backend = "trame" + pv.global_theme.smooth_shading = True + + pv.start_xvfb() + + pvmesh = meshbox.mesh2pyvista(elementType=vtk.VTK_TRIANGLE) + + with meshbox.access(): + usol = stokes.u.data.copy() + + pvmesh.point_data["mT1"] = uw.function.evaluate( + ad._u_star_projected.fn, meshbox.data + ) + pvmesh.point_data["T1"] = uw.function.evaluate(T1.fn, meshbox.data) + pvmesh.point_data["dT1"] = uw.function.evaluate( + T1.fn - ad._u_star_projected.fn, meshbox.data + ) + + arrow_loc = np.zeros((stokes.u.coords.shape[0], 3)) + arrow_loc[:, 0:2] = stokes.u.coords[...] + + arrow_length = np.zeros((stokes.u.coords.shape[0], 3)) + arrow_length[:, 0:2] = usol[...] + + pl = pv.Plotter() + + # pl.add_mesh(pvmesh,'Black', 'wireframe') + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="dT1", + use_transparency=False, + opacity=0.5, + ) + + # pl.add_arrows(arrow_loc, arrow_length, mag=1.0e-4, opacity=0.5) + # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1) + + # pl.add_points(pdata) + + pl.show(cpos="xy") + + +# - + + +def plot_T_mesh(filename): + + if uw.mpi.size == 1: + + import numpy as np + import pyvista as pv + import vtk + + pv.global_theme.background = "white" + pv.global_theme.window_size = [750, 750] + pv.global_theme.antialiasing = True + pv.global_theme.jupyter_backend = "trame" + pv.global_theme.smooth_shading = False + pv.global_theme.camera["viewup"] = [0.0, 1.0, 0.0] + pv.global_theme.camera["position"] = [0.0, 0.0, 5.0] + + pvmesh = meshbox.mesh2pyvista(elementType=vtk.VTK_TRIANGLE) + + points = np.zeros((t_soln.coords.shape[0], 3)) + points[:, 0] = t_soln.coords[:, 0] + points[:, 1] = t_soln.coords[:, 1] + + point_cloud = pv.PolyData(points) + + with meshbox.access(): + point_cloud.point_data["T"] = t_soln.data.copy() + + with swarm.access(): + points = np.zeros((swarm.data.shape[0], 3)) + points[:, 0] = swarm.data[:, 0] + points[:, 1] = swarm.data[:, 1] + + swarm_point_cloud = pv.PolyData(points) + + with swarm.access(): + swarm_point_cloud.point_data["T1"] = T1.data.copy() + + with meshbox.access(): + usol = stokes.u.data.copy() + + pvmesh.point_data["T"] = uw.function.evaluate(t_soln.fn, meshbox.data) + + arrow_loc = np.zeros((stokes.u.coords.shape[0], 3)) + arrow_loc[:, 0:2] = stokes.u.coords[...] + + arrow_length = np.zeros((stokes.u.coords.shape[0], 3)) + arrow_length[:, 0:2] = usol[...] + + pl = pv.Plotter() + + pl.add_arrows(arrow_loc, arrow_length, mag=0.00001, opacity=0.75) + + pl.add_points( + swarm_point_cloud, # cmap="RdYlBu_r", scalars="T1", + color="Black", + render_points_as_spheres=True, + clim=[0.0, 1.0], + point_size=1.0, + opacity=0.5, + ) + + pl.add_points( + point_cloud, + cmap="coolwarm", + scalars="T", + render_points_as_spheres=False, + clim=[0.0, 1.0], + point_size=10.0, + opacity=0.66, + ) + + # pl.add_mesh(pvmesh, cmap="coolwarm", edge_color="Black", + # show_edges=True, scalars="T",clim=[0.0,1.0], + # use_transparency=False, opacity=0.5) + + pl.remove_scalar_bar("T") + # pl.remove_scalar_bar("T1") + + pl.screenshot( + filename="{}.png".format(filename), + window_size=(1250, 1250), + return_img=False, + ) + # pl.show() + pl.close() + + +# + +# Convection model / update in time + +expt_name = "output/Ra1e6_swarm_pnots" + +ad_delta_t = 0.000033 # target + +for step in range(0, 250): + + stokes.solve(zero_init_guess=False) + stokes_delta_t = 5.0 * stokes.estimate_dt() + delta_t = stokes_delta_t + + ad.solve(timestep=delta_t, zero_init_guess=True) + + # update swarm / swarm variables + + with swarm.access(T1): + T1.data[:, 0] = uw.function.evaluate(t_soln.fn, swarm.particle_coordinates.data) + + # advect swarm + swarm.advection(v_soln.fn, delta_t) + + tstats = t_soln.stats() + tstarstats = T1._meshVar.stats() + + if uw.mpi.rank == 0: + print("Timestep {}, dt {}".format(step, delta_t)) + print(tstats[2], tstats[3]) + print(tstarstats[2], tstarstats[3]) + + plot_T_mesh(filename="{}_step_{}".format(expt_name, step)) + +# savefile = "{}_ts_{}.h5".format(expt_name,step) +# meshbox.save(savefile) +# v_soln.save(savefile) +# t_soln.save(savefile) +# meshbox.generate_xdmf(savefile) + +# - + + +# savefile = "output_conv/convection_cylinder.h5".format(step) +# meshbox.save(savefile) +# v_soln.save(savefile) +# t_soln.save(savefile) +# meshbox.generate_xdmf(savefile) + + +# + + + +if uw.mpi.size == 1: + + import numpy as np + import pyvista as pv + import vtk + + pv.global_theme.background = "white" + pv.global_theme.window_size = [750, 750] + pv.global_theme.antialiasing = True + pv.global_theme.jupyter_backend = "trame" + pv.global_theme.smooth_shading = True + + pv.start_xvfb() + + pvmesh = meshbox.mesh2pyvista(elementType=vtk.VTK_TRIANGLE) + + points = np.zeros((t_soln.coords.shape[0], 3)) + points[:, 0] = t_soln.coords[:, 0] + points[:, 1] = t_soln.coords[:, 1] + + point_cloud = pv.PolyData(points) + + with swarm.access(): + points = np.zeros((swarm.data.shape[0], 3)) + points[:, 0] = swarm.data[:, 0] + points[:, 1] = swarm.data[:, 1] + + swarm_point_cloud = pv.PolyData(points) + + with swarm.access(): + swarm_point_cloud.point_data["T1"] = T1.data.copy() + + with meshbox.access(): + point_cloud.point_data["T"] = t_soln.data.copy() + + with meshbox.access(): + usol = stokes.u.data.copy() + + pvmesh.point_data["T"] = uw.function.evaluate(t_soln.fn, meshbox.data) + + arrow_loc = np.zeros((stokes.u.coords.shape[0], 3)) + arrow_loc[:, 0:2] = stokes.u.coords[...] + + arrow_length = np.zeros((stokes.u.coords.shape[0], 3)) + arrow_length[:, 0:2] = usol[...] + + pl = pv.Plotter() + + pl.add_arrows(arrow_loc, arrow_length, mag=0.00002, opacity=0.75) + # pl.add_arrows(arrow_loc2, arrow_length2, mag=1.0e-1) + + # pl.add_points(point_cloud, cmap="coolwarm", + # render_points_as_spheres=True, + # point_size=7.5, opacity=0.25 + # ) + + pl.add_points( + swarm_point_cloud, + cmap="coolwarm", + render_points_as_spheres=True, + point_size=2.5, + opacity=0.5, + clim=[0.0, 1.0], + ) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="T", + use_transparency=False, + opacity=0.5, + clim=[0.0, 1.0], + ) + + pl.show(cpos="xy") +# - + + diff --git a/main/_sources/Notebooks/Examples-Utilities/Ex_UsingCoordinates.py b/main/_sources/Notebooks/Examples-Utilities/Ex_UsingCoordinates.py new file mode 100644 index 0000000..00995ba --- /dev/null +++ b/main/_sources/Notebooks/Examples-Utilities/Ex_UsingCoordinates.py @@ -0,0 +1,240 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.15.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + + +# + +import petsc4py +from petsc4py import PETSc + +import underworld3 as uw +import numpy as np +import sympy + +import os + +os.environ["SYMPY_USE_CACHE"] = "no" + +res = 0.1 +r_o = 1.0 +r_i = 0.5 +free_slip_upper = True + + +# + +meshdisc_xyz = uw.meshing.Annulus(radiusOuter=r_o, radiusInner=r_i, cellSize=res) + +mesh = meshdisc_xyz +PXs = sympy.Function(r"\mathcal{P}")(*mesh.N.base_scalars()[0 : mesh.dim]) +VXs = sympy.Matrix.zeros(1, mesh.dim) +for i in range(mesh.dim): + VXs[i] = sympy.Function(rf"\mathcal{{V}}_{i}")(*mesh.N.base_scalars()[0 : mesh.dim]) +display(PXs) +display(VXs) +# - + +# ## Symbolic forms v. mesh variables +# +# Mesh variables are `sympy.Function` objects that have the additional property of carrying data that allows them to be interploated to any point on a mesh and numerically differentiated (once). +# +# The symbolic forms allow us to undertake all the manipulations and simplifications available in sympy which are not all available for mesh variables (such as higher-order derivatives). +# +# For example, we can demonstrate some vector calculus results which we can use while we develop our equation systems. We can substitute for the mesh variables later if we choose. + +# + +gradPXs = meshdisc_xyz.vector.gradient(PXs) +display(gradPXs) + +divgradPXs = meshdisc_xyz.vector.divergence(gradPXs) +display(divgradPXs) + +curlgradPXs = meshdisc_xyz.vector.curl(gradPXs) +display(curlgradPXs) + +# + +## Create a Cylindrical Mesh with a Native Coordinate System + +meshball_xyz_tmp = uw.meshing.Annulus(radiusOuter=r_o, radiusInner=r_i, cellSize=res) + +xy_vec = meshball_xyz_tmp.dm.getCoordinates() +xy = xy_vec.array.reshape(-1, 2) +dmplex = meshball_xyz_tmp.dm.clone() +rtheta = np.empty_like(xy) +rtheta[:, 0] = np.sqrt(xy[:, 0] ** 2 + xy[:, 1] ** 2) +rtheta[:, 1] = np.arctan2(xy[:, 1] + 1.0e-16, xy[:, 0] + 1.0e-16) +rtheta_vec = xy_vec.copy() +rtheta_vec.array[...] = rtheta.reshape(-1)[...] +dmplex.setCoordinates(rtheta_vec) +meshball_xyz_tmp.vtk("tmp_disk.vtk") +del meshball_xyz_tmp + +meshdisc = uw.meshing.Mesh( + dmplex, + coordinate_system_type=uw.coordinates.CoordinateSystemType.CYLINDRICAL2D_NATIVE, +) +uw.cython.petsc_discretisation.petsc_dm_set_periodicity( + meshdisc.dm, [0.0, 1.0], [0.0, 0.0], [0.0, 2 * np.pi] +) + + +## Add some mesh variables (Vector and scalar) + +VC = uw.discretisation.MeshVariable(r"U^c", meshdisc, 2, degree=2) +PC = uw.discretisation.MeshVariable(r"P^c", meshdisc, 1, degree=1) + +## Create some symbolic equivalents + +mesh = meshdisc +PCs = sympy.Function(r"\mathcal{P}")(*mesh.N.base_scalars()[0 : mesh.dim]) +VCs = sympy.Matrix.zeros(1, mesh.dim) +for i in range(mesh.dim): + VCs[i] = sympy.Function(rf"\mathcal{{V}}_{i}")(*mesh.N.base_scalars()[0 : mesh.dim]) +display(PCs) +display(VCs) + +# + +gradPCs = meshdisc.vector.gradient(PCs) +display(gradPCs) + +divgradPCs = meshdisc.vector.divergence(gradPCs) +display(divgradPCs) + +curlgradPCs = meshdisc.vector.curl(gradPCs) +display(curlgradPCs) + +# + +## Create a Spherical Mesh with a Native Coordinate System + +## NOTE: this only works if numElements is an odd number + +meshball_xyz_tmp = uw.meshing.CubedSphere( + radiusOuter=r_o, radiusInner=r_i, numElements=7, simplex=True +) + +xyz_vec = meshball_xyz_tmp.dm.getCoordinates() +xyz = xyz_vec.array.reshape(-1, 3) +dmplex = meshball_xyz_tmp.dm.clone() + +rl1l2 = np.empty_like(xyz) +rl1l2[:, 0] = np.sqrt(xyz[:, 0] ** 2 + xyz[:, 1] ** 2 + xyz[:, 2] ** 2) +rl1l2[:, 1] = np.arctan2(xyz[:, 1], xyz[:, 0]) +rl1l2[:, 2] = ( + np.arctan2(np.sqrt(xyz[:, 0] ** 2 + xyz[:, 1] ** 2), xyz[:, 2]) - np.pi / 2 +) + +rl1l2_vec = xyz_vec.copy() +rl1l2_vec.array[...] = rl1l2.reshape(-1)[...] +dmplex.setCoordinates(rl1l2_vec) + +meshball_xyz_tmp.vtk("tmp_sphere.vtk") +del meshball_xyz_tmp + +meshball = uw.meshing.Mesh( + dmplex, coordinate_system_type=uw.coordinates.CoordinateSystemType.SPHERICAL_NATIVE +) +uw.cython.petsc_discretisation.petsc_dm_set_periodicity( + meshball.dm, [0.0, 6.28, 0.0], [0.0, 0.0, 0.0], [0.0, 2 * np.pi, 0.0] +) + + +## Add some mesh variables (Vector and scalar) + +VS = uw.discretisation.MeshVariable(r"U^s", meshball, 3, degree=2) +PS = uw.discretisation.MeshVariable(r"P^s", meshball, 1, degree=1) + +## Create some symbolic equivalents + +mesh = meshball +PSs = sympy.Function(r"\mathcal{P}")(*mesh.N.base_scalars()) +VSs = sympy.Matrix.zeros(1, mesh.dim) +for i in range(mesh.dim): + VSs[i] = sympy.Function(rf"\mathcal{{V}}_{i}")(*mesh.N.base_scalars()) +display(PSs) +display(VSs) + +# + +gradPSs = meshball.vector.gradient(PSs) +display(gradPSs) + +divVSs = meshball.vector.divergence(VSs) +display(divVSs) + +divgradPSs = meshball.vector.divergence(gradPSs) +display(divgradPSs) + +curlgradPSs = meshball.vector.curl(gradPSs) +display(curlgradPSs) + +# Note +sympy.simplify(curlgradPSs) + +# + +# if uw.mpi.size == 1: + +# import numpy as np +# import pyvista as pv +# import vtk + +# pv.global_theme.background = "white" +# pv.global_theme.window_size = [1000, 1000] +# pv.global_theme.antialiasing = True +# pv.global_theme.jupyter_backend = "panel" +# pv.global_theme.smooth_shading = True + +# pvmesh = pv.read("tmp_sphere.vtk") + +# pl = pv.Plotter(window_size=(750, 750)) + +# pl.add_mesh(pvmesh,'Black', 'wireframe') + +# pl.show(cpos="xy") +# - + +display(meshball.CoordinateSystem.rRotN) +display(meshball.CoordinateSystem.xRotN) + +# + +# We can validate using the pure symbolic forms + +gradPSs = meshball.vector.gradient(PSs) +divVSs = meshball.vector.divergence(VSs) +curlVSs = meshball.vector.curl(VSs) + +sympy.simplify(meshball.vector.divergence(curlVSs)) +# - + +sympy.simplify(meshball.vector.gradient(divVSs)) + +gradPSs + +divVSs + + +gradPs = meshball.vector.gradient(PS.sym) +gradPs + +divVs = meshball.vector.divergence(VS.sym) +divVs + +curlVs = meshball.vector.curl(VS.sym) +curlVs + +meshball.vector.strain_tensor(VS.sym) + +PS.sym.diff(meshball.CoordinateSystem.N[0]) + +PS.sym + +sympy.simplify(meshball.vector.divergence(meshball.vector.gradient(P))) + +meshball.N.base_vectors() diff --git a/main/_sources/Notebooks/Examples-Utilities/Ex_scaling.py b/main/_sources/Notebooks/Examples-Utilities/Ex_scaling.py new file mode 100644 index 0000000..ec80c25 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Utilities/Ex_scaling.py @@ -0,0 +1,176 @@ +# %% [markdown] +# # Underworld scaling example +# +# How to utilise the scaling functionality that is included with UW to easily convert between dimensional and non-dimensional values + +# %% [markdown] +# +# + +# %% +import numpy as np +import underworld3 as uw + + +# %% +# import unit registry to make it easy to convert between units +u = uw.scaling.units + +### make scaling easier +ndim = uw.scaling.non_dimensionalise +dim = uw.scaling.dimensionalise + +# %% +### reference values +length = 100. #* u.kilometer +kappa = 1e-6 #* u.meter**2/u.second +g = 9.81 #* u.meter/u.second**2 +v = 1 # u.centimeter/u.year, velocity in cm/yr +alpha = 3.0e-5 # *1./u.kelvin +tempMax = 1573.15 # * u.kelvin +tempMin = 273.15 #* u.kelvin +rho0 = 3300.0 #* u.kilogram / u.metre**3# * 9.81 * u.meter / u.second**2 +R = 8.3145 # [J/(K.mol)], gas constant + +# %% [markdown] +# Create the fundamental values required to obtain scaling for all units + +# %% +lengthScale = length * u.kilometer +surfaceTemp = tempMin * u.degK +baseModelTemp = tempMax * u.degK +bodyforce = rho0 * u.kilogram / u.metre**3 * g * u.meter / u.second**2 + +half_rate = v * u.centimeter / u.year + +KL = lengthScale.to_base_units() +Kt = (KL / half_rate).to_base_units() +KM = (bodyforce * KL**2 * Kt**2).to_base_units() +KT = (baseModelTemp - surfaceTemp).to_base_units() + + +# %% +scaling_coefficients = uw.scaling.get_coefficients() + +scaling_coefficients["[length]"] = KL.to_base_units() +scaling_coefficients["[time]"] = Kt.to_base_units() +scaling_coefficients["[mass]"] = KM.to_base_units() +scaling_coefficients["[temperature]"] = KT.to_base_units() + + +scaling_coefficients + +# %% +### fundamental values +ref_length = uw.scaling.dimensionalise(1., u.meter).magnitude + +ref_length_km = uw.scaling.dimensionalise(1., u.kilometer).magnitude + +ref_density = uw.scaling.dimensionalise(1., u.kilogram/u.meter**3).magnitude + +ref_gravity = uw.scaling.dimensionalise(1., u.meter/u.second**2).magnitude + +ref_temp = uw.scaling.dimensionalise(1., u.kelvin).magnitude + +ref_velocity = uw.scaling.dimensionalise(1., u.meter/u.second).magnitude + +### derived values +ref_time = ref_length / ref_velocity + +ref_pressure = ref_density * ref_gravity * ref_length + +ref_stress = ref_pressure + +ref_viscosity = ref_pressure * ref_time + +### Key ND values +ND_diffusivity = kappa / (ref_length**2/ref_time) +ND_gravity = g / ref_gravity + + +# %% +if uw.mpi.rank == 0: + print(f'time scaling: {ref_time/(60*60*24*365.25*1e6)} [Myr]') + print(f'pressure scaling: {ref_pressure/1e6} [MPa]') + print(f'viscosity scaling: {ref_viscosity} [Pa s]') + print(f'velocity scaling: {ref_velocity*(1e2*60*60*24*365.25)} [cm/yr]') + print(f'length scaling: {ref_length/1e3} [km]') + +# %% [markdown] +# ### How to non-dimensionalise a value + +# %% +cohesion = 10e6 + +# %% +### first way, using the UW non-dimensionalise function, have to provide the units +nd_cohesion = ndim(cohesion * u.pascal) +nd_cohesion + +# %% +#### second way, just divide cohesion by the scaled reference value +cohesion/ref_stress + +# %% +#### check if they are close +np.isclose(ndim(cohesion * u.pascal), (cohesion/ref_pressure), rtol=1e-10, atol=1e-10) + +# %% +viscosity = 1e21 + +# %% +nd_visc = ndim(viscosity * u.pascal*u.second) + +nd_visc + +# %% +viscosity / ref_viscosity + +# %% +np.isclose(ndim(viscosity * u.pascal*u.second), (viscosity / ref_viscosity), rtol=1e-10, atol=1e-10) + +# %% [markdown] +# # How to dimensionalise + +# %% +### can either use the inbuilt function, where you provide the units + +dim(nd_visc, u.pascal*u.second) + +# %% +#### or multiply by the ref value which has units of Pa s +nd_visc * ref_viscosity + +# %% +### you can remove the units by using the magnitude function +print(dim(nd_visc, u.pascal*u.second).magnitude) + +#### or use .m (short for .magnitude) +print(dim(nd_visc, u.pascal*u.second).m) + +# %% +#### check if they give the same answers +np.isclose(dim(nd_visc, u.pascal*u.second).m, nd_visc * ref_viscosity, rtol=1e-10, atol=1e-10) + +# %% [markdown] +# ### check mesh and swarm vars work +# +# Currently not supported + +# %% +# Set the resolution. +res = 32 +xmin, xmax = 0, 1 +ymin, ymax = 0, 1 + +mesh = uw.meshing.StructuredQuadBox(elementRes=(int(res), int(res)), minCoords=(xmin, ymin), maxCoords=(xmax, ymax)) + + + +# %% +dim(mesh.data, u.kilometer) + +# %% +mesh.data + +# %% diff --git a/main/_sources/Notebooks/Examples-Utilities/Readme.md b/main/_sources/Notebooks/Examples-Utilities/Readme.md new file mode 100644 index 0000000..edc18c0 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Utilities/Readme.md @@ -0,0 +1,15 @@ +# Miscellaneous Tips and Tricks + +## Recent solver and visualization updates + +- [x] Ex_Integrals_on_Meshes.py + - [ ] RuntimeError: The mesh requires at least a single variable for integration to function correctly. This is a PETSc limitation. Create a ticket. +- [ ] Ex_Tensor_Variables_etc.py + - [ ] Temperature mesh variable not defined and require changes in advDiff system +- [x] Ex_Projection_Function_Eval.py +- [x] Ex_UsingCoordinates.py +- [x] Ex_scaling.py +- [x] Ex_Anisotropy.py +- [x] Ex_BCs_from_meshVariables.py +- [ ] Ex_ConstitutiveTensors.py + - [ ] AttributeError: type object 'TransverseIsotropicFlowModel' has no attribute 'Parameters' diff --git a/main/_sources/Notebooks/Examples-Utilities/output/README.md b/main/_sources/Notebooks/Examples-Utilities/output/README.md new file mode 100644 index 0000000..eecb7c4 --- /dev/null +++ b/main/_sources/Notebooks/Examples-Utilities/output/README.md @@ -0,0 +1,4 @@ +# Utilities + model outputs + +These files are NOT under version control \ No newline at end of file diff --git a/main/_sources/Notebooks/Index.md b/main/_sources/Notebooks/Index.md new file mode 100644 index 0000000..fe37c80 --- /dev/null +++ b/main/_sources/Notebooks/Index.md @@ -0,0 +1,43 @@ +# Index of Sample notebooks + +Typically, a markdown (`myst` format) file is used for text that is not going to be executed as a notebook. Notebooks are organised into a number of folders: + + - Examples-Convection + - Examples-Meshing + - Examples-NavierStokes + - Examples-PoissonEquation + - Examples-PorousFlow + - Examples-Sandbox-VEP + - Examples-StokesFlow + - Examples-UW-2to3 + - Examples-Utilities + + + + +## Python packages + +Any packages that you need to have installed by `conda` or `pip` should be included in the `conda_packages.yml` file in the root directory of the repository. You can also specify packages for the `apt get` system if they cannot be installed by `conda`. + + +```python + + + +``` + +```python + +``` diff --git a/main/_sources/Theory/FiniteElements.md b/main/_sources/Theory/FiniteElements.md new file mode 100644 index 0000000..438aceb --- /dev/null +++ b/main/_sources/Theory/FiniteElements.md @@ -0,0 +1,12 @@ +# The Lagrangian-Particle FEM + +Motivation + +Relation to classical FEM, classical FEM derivations + +Relation to MPM etc + +(Grab notes from numerical methods "book") + +Details of implementation + diff --git a/main/_sources/Theory/Index.md b/main/_sources/Theory/Index.md new file mode 100644 index 0000000..4696c47 --- /dev/null +++ b/main/_sources/Theory/Index.md @@ -0,0 +1,6 @@ +# Underworld Theory Manual + +Background theory, in common with Underworld publications and with full references. + + +Not sure whether to put comp. sci. theory in here like how the functions work ... maybe in their own section ? \ No newline at end of file diff --git a/main/_sources/Theory/LinearAlgebra.md b/main/_sources/Theory/LinearAlgebra.md new file mode 100644 index 0000000..f20778e --- /dev/null +++ b/main/_sources/Theory/LinearAlgebra.md @@ -0,0 +1,4 @@ +# Linear Algebra + +The realisation of the Finite Elements and the Mathematical models in the form of matrices, combined with our strategies for solving them. + diff --git a/main/_sources/Theory/MathematicalBackground.md b/main/_sources/Theory/MathematicalBackground.md new file mode 100644 index 0000000..7b5fd84 --- /dev/null +++ b/main/_sources/Theory/MathematicalBackground.md @@ -0,0 +1,9 @@ +# Mathematical Background + +Equation systems + +Constitutive laws + +Elasticity, Plasticity formulation + + diff --git a/main/_sources/Theory/NonLinear.md b/main/_sources/Theory/NonLinear.md new file mode 100644 index 0000000..3518ef2 --- /dev/null +++ b/main/_sources/Theory/NonLinear.md @@ -0,0 +1,5 @@ +# Non Linear Methods + +TBA + +SNES stuff would be good here. \ No newline at end of file diff --git a/main/_sources/Theory/PointwiseFunctions.md b/main/_sources/Theory/PointwiseFunctions.md new file mode 100644 index 0000000..1a66bd7 --- /dev/null +++ b/main/_sources/Theory/PointwiseFunctions.md @@ -0,0 +1,557 @@ +# PETSc pointwise functions and PDE solvers + +As we saw in [the Finite Element Pages], the finite element method provides a very general way to approach the numerical solution of a very wide variety of problems in continuum mechanics using a standardised, matrix-based formulation and with considerable flexibility in the choice of discretisation, mesh geometry, and the ability to deal very naturally with jumps in material properties. + +However, the FEM is based upon a variational, "weak" form of the governing equations of any problem while most publications outline the strong form of any problem and this can make it difficult for anyone without a strong mathematical background to translate quickly from publication to a new model. + +PETSc provides a mechanism to automatically generate a finite element weak form from the strong (point-wise) form of the governing equations. This takes the form of a template equation and, in the case of non-linear problems, a template for the corresponding Jacobian derivatives. + +The PETSc strong-form interface asks the user to provide pre-compiled functions with a pre-defined pattern of arguments relating known nodal-point variables, shape-functions and derivatives at any point in the domain. If the strong form is written + +$$ \mathbf{F}(\mathbf{u}) \sim \nabla.\mathbf{f}_1(u, \nabla u) - f_0 (u, \nabla u) = 0 $$ + +Where the $f_0$ term, generally speaking, represents the forces and $f_1$ comprises flux terms. +Then a corresponding weak form is + +$$ \phi^T \mathbf{F}(\mathbf{u}) \sim \int_\Omega \phi \cdot f_0 \left(u, \nabla u \right) + + \nabla \phi \mathbf{f}_1 \left(u, \nabla u \right) = 0 $$ + + +The discrete form of this equation has some obvious similarities to the standard finite element matrix form except that the functions $f_0$ and $\mathbf{f}_1$ are not directly expressed in terms of the basis function matrices: + +$$ \cal{F}(\cal{u}) \sim \sum_e \epsilon_e^T \left[ B^T W f_0(u^q, \nabla u^q) + + \sum_k D_k^T W \mathbf{f}_1^k (u^q, \nabla u^q) \right] = 0 +$$ + +where $q$ represents a set of quadrature points, $W$ is a matrix of weights and $B$, $D$ are the usual basis function matrices but here evaluated at the quadrature points. $\epsilon$ restricts the terms to the element. See {cite}`knepleyAchievingHighPerformance2013` for details. + +The user must provide a compiled representation of the terms $f_0$ and $\mathbf{f}_1$ but it is also necessary to provide the corresponding compiled representations of the Jacobians which satisfy + +$$ \cal{F'}(\cal{u}) \sim \sum_e \epsilon_e^T + \left[ \begin{array}{cc} + B^T & \mathbf{D}^T \\ + \end{array} \right] + \mathbf{W} + \left[ \begin{array}{cc} + f_{0,0} & f_{0,1} \\ + \mathbf{f}_{1,0} & \mathbf{f}_{1,1} \\ + \end{array}\right] + \left[ \begin{array}{c} + B^T \\ + \mathbf{D}^T + \end{array} \right] \epsilon_e +\quad \mathrm{and} \quad + f_{[i,j]} = + \left[ \begin{array}{cc} + \partial f_0 / \partial u & \partial f_0 / \partial \nabla u \\ + \partial \mathbf{f}_1 / \partial u & \partial \mathbf{f}_1 / \partial \nabla u + \end{array}\right] +$$ + +This formulation greatly simplifies writing optimal code as the user sets up a problem without touching the bulk of the implementation. The only difficulty is ensuring that the Jacobians are derived consistently and correctly. As the Jacobians need to be calculated and implemented as separate functions this introduces a potential point of failure. + +In `underworld`, we provide a fully symbolic approach to specifying the strong form terms, $f_0$ and $\mathbf{f}_1$ using the `sympy` symbolic algebra python package. `sympy` is also able to differentiate +$f_0$ and $\mathbf{f}_1$ to obtain the relevant Jacobian matrices in symbolic form. We also take +advantage of `sympy`s automatic code generation capabilities to compile functions that match +the PETSc templates. + +This provides a very natural mapping between the formulation above and the `sympy` python code. + +```python + + # X and U are sympy position vector, unknown vector + # that may include mesh variables + + U_grad = sympy.derive_by_array(U, X) + + F0 = f + F1 = kappa * U_grad + + # Jacobians are + + J_00 = sympy.derive_by_array(F0, U) + J_01 = sympy.derive_by_array(F0, U_grad) + J_10 = sympy.derive_by_array(F1, U) + J_11 = sympy.derive_by_array(F1, U_grad) + + # Pass F0, F1, J_xx to sympy code generation routines + + # ... + +``` + +Note: some re-indexing may be necessary to interface between `sympy` and PETSc +especially for vector problems. + +Note: underworld provides a translation between mesh variables and their `sympy` +symbolic representation on the user-facing side that also needs to translate +to PETSc data structures in the compiled code. + +## Underworld Solver Classes + +We provide 3 base classes to build solvers. These are a scalar SNES solver, +a vector SNES solver and a Vector SNES saddle point solver (constrained vector problem). +These are bare-bones classes that implement the pointwise function / sympy approach that +can then be used to build solvers for many common situations. + +A blank slate is a very scary thing and so we provide templates for some common equations +and examples to show how these can be extended. + + +`````{tabbed} Poisson Equation + +````{panels} +Equation +^^^ +$$ \nabla \cdot (\alpha \nabla \psi) = \rho $$ + +--- + +`sympy` expression +^^^ +```python +grad_psi = sympy.vector.gradient(solver.psi) +solver.F0 = uw_fn_rho +solver.F1 = -uw_fn_alpha * grad_psi +``` +```` +````` + +`````{tabbed} Projections +````{panels} +Equation +^^^ +Solve for $u$ on the mesh unknowns that best satisfy $\tilde{u}$ +evaluated on points within the mesh. + +$$ \int_\Omega \phi u d\Omega = \int_\Omega \phi \tilde{u} d\Omega $$ + +--- + +`sympy` expression +^^^ +```python +solver.F0 = solver.u - uw_function_u_tilde +solver.F1 = 0.0 +``` +```` +````` + +`````{tabbed} Incompressible Stokes +````{panels} +Equation +^^^ + +The momentum balance equation is + +$$ \nabla \cdot \left(\mathbf{\tau} - p \mathbf{I}\right) = f_\textrm{buoy} $$ + +with an incompressible flow constraint + +$$ \nabla \cdot \mathbf{u} = 0 $$ + +and the deviatoric stress defined as + +$$ \tau = \eta \left( \nabla \mathbf{u} + \nabla \mathbf{u}^T \right) $$ + + +--- + +`sympy` expression +^^^ +```python +grad_U = sympy.derive_by_array(solver.U, solver.X) +grad_U_T = grad_U.transpose() +epsdot = (grad_U + grad_U_T) + +solver.UF0 = -uw_fn_buoyancy_force +solver.UF1 = u_fn_viscosity * epsdot - \ + sympy.eye(dim) * solver.P + +# constraint equation +solver.PF0 = sympy.vector.divergence(solver.U) +``` +```` + +````` + +`````{tabbed} Advection-Diffusion Equations +````{panels} +Equation +^^^ + +$$ \frac{\partial \psi}{\partial t} + \mathbf{u}\cdot\nabla\psi = \nabla \cdot \alpha \nabla \psi $$ + +or, in Lagrangian form (following $\mathbf{u}$), + +$$ \frac{D \psi}{D t} = \nabla \cdot \alpha \nabla \psi $$ + +and this approximation for the time derivative + +$$ \frac{D \psi}{Dt} \approx \frac{\psi_p - \psi^*}{\Delta t}$$ + +where $\psi^*$ is the value upstream at $t-\Delta t$ + + + +--- + +`sympy` expression +^^^ +```python + +grad_U = sympy.derive_by_array(solver.U, solver.X) +grad_Us = sympy.derive_by_array(solver.U_star, solver.X) +DUDt = (solver.U - solver.U_star) / delta_t + +solver.F0 = DUdt +solver.F1 = uw_fn_alpha * (grad_U + grad_Us) / 2 + +``` +```` +````` + +## Implementation & Examples + +### Poisson Solvers + +(link to another document) +Diffusion + +Darcy flow + +Advection-diffusion (SLCN) + +### Advection dominated flow + +(link to another document) + +Swarm-based problems + +Projection / swarm evaluation + +Advection-diffusion (Swarm) + +Material point methods + +### Incompressible Stokes + +(link to another document) + +Saddle point problems + +Stokes, boundary conditions, constraints + +Navier-Stokes (Swarm) + +Viscoelasticity + +## Remarks + +The generic solver classes can be used to construct all of the examples above. The equation-system classes that we provide help to provide a template or scaffolding for a less experienced user and they also help to orchestrate cases where multiple solvers come together in a specific order (e.g. the Navier-Stokes case where history variable +projections need to be evaluated during the solve). + +Creating sub-classes from the equation systems or the generic solvers is an excellent way to build workflows whenever there is a risk of exposing some fragile construction at the user level. + +Some of the need for these templates is a result of inconsistencies in the way `sympy` treats matrices, vectors and tensor (array) objects. We expect this to change over time. + + +## Example 1 - The Poisson Equation + +The classical form of the scalar Poisson Equation is + +$$ \alpha \nabla^2 \psi = f $$ + +Where $\psi$ is an unknown scalar quantity, $\alpha$ is +a constitutive parameter that relates gradients to fluxes, and $f$ +is a source term. + +This equation is obtained by considering the divergence of fluxes needed to +balance the sources. For example, in thermal diffusion we identify +$\psi$ with the temperature, $T$, and the constitutive parameter, $k$, +is a thermal conductivity. + +$$ \nabla \cdot k \nabla T = h $$ + +In this form, $\mathbf{q} = k \nabla T$ is Fourier's expression of the +heat flux in terms of temperature gradients.This form matches the template above if we identify: + +$$ f_0 = -h \quad \textrm{and} \quad f_1 = k\nabla T$$ + +and, in fact, this is exactly what we need to specify in the underworld equation +system. + +```python + solver.L= sympy.derive_by_array(solver.U, solver.X).transpose() + + # f0 residual term (weighted integration) - scalar function + solver.F0 = -h + + # f1 residual term (integration by parts / gradients) + solver.F1 = k * solver.L +``` + +which means the user only needs to supply a mesh, a mesh variable to +hold the solution and sympy expressions for $k$ and $h$ in order +to solve a Poisson equation. + +The `SNES_Poisson` class is a very lightweight wrapper on +the `SNES_Scalar` class which provides a template for the flux +term and very little else. +$F_0$ and $F_1$ are inherited as an empty scalar and vector respectively. +These are available in the template for the user to extend the equation as needed. + +[This notebook](../Notebooks/Ex_Poisson_Cartesian_Generic) compares +the generic class and the one with the flux templated. + +## Example 2 - Projections and Evaluations + +PETSc has a very general concept of discretisation spaces that do not +necessarily admit to continuous interpolation to or from arbitrary points. +For this reason, a more general concept is to create projections that map +between representations of the data. For example, in Finite Elements, +fluxes are generally not available at nodal points because shape functions +have discontinuous gradients there. To compute fluxes at nodal points, we +would establish a projection problem to form a best fitting continous function +to the values at points where we can evaluate the fluxes. In addition, +sympy functions (including those for fluxes) that contain derivatives of finite element variables +can not be evaluated numerically by sympy but can be evaluated as compiled +functions in the context of a solver. + +We write these evaluations using the `Projection` solver classes. This is +the simplest of the solvers and we are only discussing it second because it +is almost too simple to be instructive (and because the weak form of this +equation is the natural one to work with). + +We would like to solve for a continuous, nodal point solution $u$ that +satisfies as best possible, + +$$ \int_\Omega \phi u d\Omega = \int_\Omega \phi \tilde{u} d\Omega $$ + +where $\tilde{u}$ is a function with unknown continuity that we +are able to evaluate at integration points in the mesh. + +The generic solver specification in underworld looks like this + +```python + + # f0 residual term (weighted integration) - scalar function + solver.F0 = solver.u.fn - user_uw_function + + # f1 residual term (integration by parts / gradients) + solver.F1 = 0.0 +``` +where `user_uw_function` is some sympy expression in terms of spatial +coordinates that can include mesh or swarm variables. `solver.u.fn` is the +mesh variable (in function form) where the solution will reside. + +In principle we could add a smoothing term via `solver.F1` and, importantly, +we can also add boundary conditions or constraints that need to be satisfied in +addition to fitting the integration point values. + +We provide projection operators for scalar fields, vector fields and +solenoidal vector fields (ensuring that the projection remains divergence free). These +provide templates for the $F_0$ and $F_1$ terms with generic smoothing. +For an explanation of the divergence free projection methodology, see the next +example on the incompressible Stokes problem. + +[This notebook](../Notebooks/Ex_Project_Function.md) has an example of +each of these cases. + +## Example 3 - Incompressible Stokes Equation + +The incompressible Stokes Equation is an example of a problem with an +additional constraint equation that must be satisfied by the solution. +Variational formulations (the weak form of +classical finite elements is one) naturally lend themselves +to the addition of multiple constraints into the functional to be minimised. + +Unfortunately, the incompressiblity constraint needs to be enforced very strongly +to obtain reasonable solutions and this can lead to unacceptably +ill conditioned systems that are slow or impossible to solve. + +Alternatively, we solve a coupled problem in which additional, kinematic, +parameters of the constraint term are introduced. This forms a new block system of +equations that is a general expression for a large range of constrained problems. + +These are often known as *saddle point problems* which represents the trade-off +between satisfying the original equations and the constraint equations. +(Saddle point here refers to the curvature of the functional we are +optimising) See: M. Benzi, G. Golub and J. Liesen, +Numerical solution of saddle point problems, Acta Numerica 14 (2005), +pp. 1–137. for a general discussion. + +The coupled equation system we want to solve is + +$$ \nabla \cdot \mathbf{\tau} - \nabla p = f_\textrm{buoy} $$ + +with the constraint + +$$ \nabla \cdot \mathbf{u} = 0 $$ + +The saddle-point solver requires us to specify both of these equations and +to provide two solution vectors $\mathbf{u}$ and $\mathbf{p}$. In this +system, $\mathbf{p}$ is the parameter that enforces the incompressiblity +constraint equation and is physically identifiable as a pressure. + +```python + + # definitions + + U_grad = sympy.derive_by_array(solver.U, solver.X) + + strainrate = (sympy.Matrix(U_grad) + sympy.Matrix(U_grad).T)/2 + stress = 2*solver.viscosity*solver.strainrate + + # set up equation terms + + # u f0 residual term (weighted integration) - vector function + solver.UF0 = - solver.bodyforce + + # u f1 residual term (integration by parts / gradients) - tensor (sympy.array) term + solver.UF1 = stress + + # p f0 residual term (these are the constraints) - vector function + + solver.PF0 = sympy.vector.divergence(solver.U) +``` + +In `underworld`, the `SNES_Stokes` solver class is responsible for managing the +user interface to the saddle point system for incompressible Stokes flow. + +## Example 4 - Advection in the absence of diffusion + +The pure transport equation can be written in Lagrangian + +$$ \frac{D \psi}{D t} = 0 $$ + +or in Eulerian form + +$$ \frac{\partial \psi}{\partial t} + \mathbf{u} \cdot \nabla \psi = 0 $$ + +In the Lagrangian form, there is nothing to solve, provided the fluid-transported reference frame is available. In the Eulerian form, the non-linear *advection* term +$\mathbf{u} \cdot \nabla \psi$ is reknowned for being difficult to solve, especially in the pure-transport form. + +Underworld provides discrete Lagrangian `swarm` variables [ CROSSREF ] that make it straightforward to work with transported quantities +on a collection of moving sample points that we normally refer to as *particles*. +Behind the scenes, there is complexity in 1) following the Lagrangian reference frame accurately, +2) mapping the fluid-deformed reference frame to the stationary mesh, and 3) for +parallel meshes, migrating particles (and their data) across the decomposed domain. + +The `swarm` that manages the variables is able to update the locations of the particles +when provided with a velocity vector field and a time increment and will handle the +particle re-distribution in the process. + +Each variable on a swarm has a corresponding mesh variable (a *proxy* variable) that +is automatically updated when the particle locations change. The proxy variable is +computed through a projection (see above). + +*Note:* If specific boundary conditions need to be applied, it is necessary for the user +to define their own projection operator, apply the boundary conditions, and solve when needed. +(*Feature request: allow user control over the projection, including +boundary conditions / constraints, so that this is not part of the user's responsibility*) + +## Example 5 - The Scalar Advection-diffusion Equation + +The situation where a quantity is diffusing through a moving fluid. + +$$ \frac{\partial \psi}{\partial t} + \mathbf{u}\cdot\nabla\psi = \nabla \cdot \alpha \nabla \psi + f$$ + +where $\mathbf{u}$ is a (velocity) vector that transports $\psi$ and $\alpha$ is a +diffusivity. In Lagrangian form (following $\mathbf{u}$), + +$$ \frac{D \psi}{D t} = \nabla \cdot \alpha \nabla \psi + f$$ + +As before, the advection terms are greatly simplified in a Lagrangian reference +frame but now we also have diffusion terms and boundary conditions that are easy +to solve accurately in an Eulerian mesh but which must also be applied to variables +that derive from a Lagrangian swarm (which has no boundary conditions of its own). + +Advection-diffusion equations are often dominated by the presence of boundary layers where +advection of a quantity (along the direction of flow) is balanced by a diffusive flux +in the cross-stream direction. Under these conditions, there is some work to be done to +ensure that these two terms are calculated consistently and this is particularly important +close to regions where boundary conditions need to be applied. + +The approach in `underworld` is to provide a solver structure to manage +advection-diffusion problems on behalf of the user. We use +a swarm-variable for tracking the history of the $\psi$ as it is transported +by $\mathbf{u}$ and we allow the user to specify (solve for) this flow, and +to update the swarm positions accordingly. The history variable, $\psi^*$ +is the value of $\psi$ upstream at an earlier timestep and allows us to +approximate $D \psi/Dt$ as a finite difference approximation along the +characteristics of the advection operator: + +$$\left. \frac{D \psi}{Dt} \right|_{p} \approx \frac{\psi_p - \psi^*_p}{\Delta t}$$ + +Here, the subscript $p$ indicates a value at a particle in the Lagrangian swarm. + +This approach leads to a very natural problem description in python that corresponds closely to the mathematical formulation, namely: + +```python + solver.L = sympy.derive_by_array(solver.U, solver.X).transpose() + solver.Lstar = sympy.derive_by_array(solver.U_star, solver.X).transpose() + + # f0 residual term + solver._f0 = -solver.f + (solver.U.fn - solver.U_star.fn) / solver.delta_t + + # f1 residual term (backward Euler) + solver._f1 = solver.L * solver.k + + ## OR + + # f1 residual term (Crank-Nicholson) + solver._f1 = 0.5 * (solver.L + solver.Lstar) * solver.k +``` + +In the above, the `U_star` variable is a projection of the Lagrangian history variable +$\psi^*_p$ onto the mesh *subject to the same boundary conditions as* $\psi$. + +In the `SNES_AdvectionDiffusion_Swarm` class (which is derived from `SNES_Poisson`), +the `solve` method solves for `U_star` using an in-built projection and boundary +conditions copied from the parent, before calling a standard Poisson solver. This class manages every aspect of the creation, refresh and solution of the necessary +projection subroutines Lagrangian history term, but not the update of this variable or the advection. + +*Caveat emptor:* In the Crank-Nicholson stiffness matrix terms above, we form the derivatives in both the flux and the flux history with the same operator where, strictly, we should transport the derivatives (or form derivatives with respect to the transported coordinate system). + +## Example 6 - Navier-Stokes + +The incompressible Navier-Stokes equation of fluid dynamics is essentially the vector equivalent of the +scalar advection-diffusion equation above, in which the transported quantity is the velocity (strictly momentum) vector that is also responsible for the transport. + +$$ \rho \frac{\partial \mathbf{u}}{\partial t} + \mathbf{u}\cdot\nabla\mathbf{u} = \nabla \cdot \eta \left( \nabla \mathbf{u} + \nabla \mathbf{u}^T \right)/2 + \rho \mathbf{g}$$ + +$$ \nabla \cdot \mathbf{u} = 0 $$ + +Obviously this is a strongly non-linear problem, but simply introduce the time dependence to the Stokes equation in the same way as we did for the Poisson equation above. A finite difference representation of the Lagrangian derivative of the velocity is defined using a vector swarm variable + +$$\left. \frac{D \mathbf{u}}{Dt} \right|_{p} \approx \frac{\mathbf{u}_p - \mathbf{u}^*_p}{\Delta t}$$ + +And the python problem description becomes: + +```python + # definitions + + U_grad = sympy.derive_by_array(solver.U, solver.X) + U_grad_star = sympy.derive_by_array(solver.Ustar, solver.X) + + strainrate = (sympy.Matrix(U_grad) + sympy.Matrix(U_grad).T)/2 + stress = 2*solver.viscosity*solver.strainrate + + strainrate_star = (sympy.Matrix(U_grad_star) + sympy.Matrix(U_grad_star).T)/2 + stress_star = 2*solver.viscosity*solver.strainrate_star + + # set up equation terms + + # u f0 residual term (weighted integration) - vector function + solver.UF0 = - solver.bodyforce + solver.rho * (solver.U.fn - solver.U_star.fn) / solver.delta_t + + # u f1 residual term (integration by parts / gradients) - tensor (sympy.array) term + solver.UF1 = 0.5 * stress * 0.5 * stress_star + + # p f0 residual term (these are the constraints) - vector function + solver.PF0 = sympy.vector.divergence(solver.U) +``` + +Note, again, that, formulated in this way, the stress and strain-rate history variables neglect terms resulting from the deformation of the coordinate system over the timestep, $\Delta t$. We could instead transport the strain rate or stress + diff --git a/main/_sphinx_design_static/design-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css b/main/_sphinx_design_static/design-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css new file mode 100644 index 0000000..eb19f69 --- /dev/null +++ b/main/_sphinx_design_static/design-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css @@ -0,0 +1 @@ +.sd-bg-primary{background-color:var(--sd-color-primary) !important}.sd-bg-text-primary{color:var(--sd-color-primary-text) !important}button.sd-bg-primary:focus,button.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}a.sd-bg-primary:focus,a.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}.sd-bg-secondary{background-color:var(--sd-color-secondary) !important}.sd-bg-text-secondary{color:var(--sd-color-secondary-text) !important}button.sd-bg-secondary:focus,button.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}a.sd-bg-secondary:focus,a.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}.sd-bg-success{background-color:var(--sd-color-success) !important}.sd-bg-text-success{color:var(--sd-color-success-text) !important}button.sd-bg-success:focus,button.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}a.sd-bg-success:focus,a.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}.sd-bg-info{background-color:var(--sd-color-info) !important}.sd-bg-text-info{color:var(--sd-color-info-text) !important}button.sd-bg-info:focus,button.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}a.sd-bg-info:focus,a.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}.sd-bg-warning{background-color:var(--sd-color-warning) !important}.sd-bg-text-warning{color:var(--sd-color-warning-text) !important}button.sd-bg-warning:focus,button.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}a.sd-bg-warning:focus,a.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}.sd-bg-danger{background-color:var(--sd-color-danger) !important}.sd-bg-text-danger{color:var(--sd-color-danger-text) !important}button.sd-bg-danger:focus,button.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}a.sd-bg-danger:focus,a.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}.sd-bg-light{background-color:var(--sd-color-light) !important}.sd-bg-text-light{color:var(--sd-color-light-text) !important}button.sd-bg-light:focus,button.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}a.sd-bg-light:focus,a.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}.sd-bg-muted{background-color:var(--sd-color-muted) !important}.sd-bg-text-muted{color:var(--sd-color-muted-text) !important}button.sd-bg-muted:focus,button.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}a.sd-bg-muted:focus,a.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}.sd-bg-dark{background-color:var(--sd-color-dark) !important}.sd-bg-text-dark{color:var(--sd-color-dark-text) !important}button.sd-bg-dark:focus,button.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}a.sd-bg-dark:focus,a.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}.sd-bg-black{background-color:var(--sd-color-black) !important}.sd-bg-text-black{color:var(--sd-color-black-text) !important}button.sd-bg-black:focus,button.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}a.sd-bg-black:focus,a.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}.sd-bg-white{background-color:var(--sd-color-white) !important}.sd-bg-text-white{color:var(--sd-color-white-text) !important}button.sd-bg-white:focus,button.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}a.sd-bg-white:focus,a.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}.sd-text-primary,.sd-text-primary>p{color:var(--sd-color-primary) !important}a.sd-text-primary:focus,a.sd-text-primary:hover{color:var(--sd-color-primary-highlight) !important}.sd-text-secondary,.sd-text-secondary>p{color:var(--sd-color-secondary) !important}a.sd-text-secondary:focus,a.sd-text-secondary:hover{color:var(--sd-color-secondary-highlight) !important}.sd-text-success,.sd-text-success>p{color:var(--sd-color-success) !important}a.sd-text-success:focus,a.sd-text-success:hover{color:var(--sd-color-success-highlight) !important}.sd-text-info,.sd-text-info>p{color:var(--sd-color-info) !important}a.sd-text-info:focus,a.sd-text-info:hover{color:var(--sd-color-info-highlight) !important}.sd-text-warning,.sd-text-warning>p{color:var(--sd-color-warning) !important}a.sd-text-warning:focus,a.sd-text-warning:hover{color:var(--sd-color-warning-highlight) !important}.sd-text-danger,.sd-text-danger>p{color:var(--sd-color-danger) !important}a.sd-text-danger:focus,a.sd-text-danger:hover{color:var(--sd-color-danger-highlight) !important}.sd-text-light,.sd-text-light>p{color:var(--sd-color-light) !important}a.sd-text-light:focus,a.sd-text-light:hover{color:var(--sd-color-light-highlight) !important}.sd-text-muted,.sd-text-muted>p{color:var(--sd-color-muted) !important}a.sd-text-muted:focus,a.sd-text-muted:hover{color:var(--sd-color-muted-highlight) !important}.sd-text-dark,.sd-text-dark>p{color:var(--sd-color-dark) !important}a.sd-text-dark:focus,a.sd-text-dark:hover{color:var(--sd-color-dark-highlight) !important}.sd-text-black,.sd-text-black>p{color:var(--sd-color-black) !important}a.sd-text-black:focus,a.sd-text-black:hover{color:var(--sd-color-black-highlight) !important}.sd-text-white,.sd-text-white>p{color:var(--sd-color-white) !important}a.sd-text-white:focus,a.sd-text-white:hover{color:var(--sd-color-white-highlight) !important}.sd-outline-primary{border-color:var(--sd-color-primary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-primary:focus,a.sd-outline-primary:hover{border-color:var(--sd-color-primary-highlight) !important}.sd-outline-secondary{border-color:var(--sd-color-secondary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-secondary:focus,a.sd-outline-secondary:hover{border-color:var(--sd-color-secondary-highlight) !important}.sd-outline-success{border-color:var(--sd-color-success) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-success:focus,a.sd-outline-success:hover{border-color:var(--sd-color-success-highlight) !important}.sd-outline-info{border-color:var(--sd-color-info) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-info:focus,a.sd-outline-info:hover{border-color:var(--sd-color-info-highlight) !important}.sd-outline-warning{border-color:var(--sd-color-warning) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-warning:focus,a.sd-outline-warning:hover{border-color:var(--sd-color-warning-highlight) !important}.sd-outline-danger{border-color:var(--sd-color-danger) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-danger:focus,a.sd-outline-danger:hover{border-color:var(--sd-color-danger-highlight) !important}.sd-outline-light{border-color:var(--sd-color-light) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-light:focus,a.sd-outline-light:hover{border-color:var(--sd-color-light-highlight) !important}.sd-outline-muted{border-color:var(--sd-color-muted) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-muted:focus,a.sd-outline-muted:hover{border-color:var(--sd-color-muted-highlight) !important}.sd-outline-dark{border-color:var(--sd-color-dark) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-dark:focus,a.sd-outline-dark:hover{border-color:var(--sd-color-dark-highlight) !important}.sd-outline-black{border-color:var(--sd-color-black) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-black:focus,a.sd-outline-black:hover{border-color:var(--sd-color-black-highlight) !important}.sd-outline-white{border-color:var(--sd-color-white) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-white:focus,a.sd-outline-white:hover{border-color:var(--sd-color-white-highlight) !important}.sd-bg-transparent{background-color:transparent !important}.sd-outline-transparent{border-color:transparent !important}.sd-text-transparent{color:transparent !important}.sd-p-0{padding:0 !important}.sd-pt-0,.sd-py-0{padding-top:0 !important}.sd-pr-0,.sd-px-0{padding-right:0 !important}.sd-pb-0,.sd-py-0{padding-bottom:0 !important}.sd-pl-0,.sd-px-0{padding-left:0 !important}.sd-p-1{padding:.25rem !important}.sd-pt-1,.sd-py-1{padding-top:.25rem !important}.sd-pr-1,.sd-px-1{padding-right:.25rem !important}.sd-pb-1,.sd-py-1{padding-bottom:.25rem !important}.sd-pl-1,.sd-px-1{padding-left:.25rem !important}.sd-p-2{padding:.5rem !important}.sd-pt-2,.sd-py-2{padding-top:.5rem !important}.sd-pr-2,.sd-px-2{padding-right:.5rem !important}.sd-pb-2,.sd-py-2{padding-bottom:.5rem !important}.sd-pl-2,.sd-px-2{padding-left:.5rem !important}.sd-p-3{padding:1rem !important}.sd-pt-3,.sd-py-3{padding-top:1rem !important}.sd-pr-3,.sd-px-3{padding-right:1rem !important}.sd-pb-3,.sd-py-3{padding-bottom:1rem !important}.sd-pl-3,.sd-px-3{padding-left:1rem !important}.sd-p-4{padding:1.5rem !important}.sd-pt-4,.sd-py-4{padding-top:1.5rem !important}.sd-pr-4,.sd-px-4{padding-right:1.5rem !important}.sd-pb-4,.sd-py-4{padding-bottom:1.5rem !important}.sd-pl-4,.sd-px-4{padding-left:1.5rem !important}.sd-p-5{padding:3rem !important}.sd-pt-5,.sd-py-5{padding-top:3rem !important}.sd-pr-5,.sd-px-5{padding-right:3rem !important}.sd-pb-5,.sd-py-5{padding-bottom:3rem !important}.sd-pl-5,.sd-px-5{padding-left:3rem !important}.sd-m-auto{margin:auto !important}.sd-mt-auto,.sd-my-auto{margin-top:auto !important}.sd-mr-auto,.sd-mx-auto{margin-right:auto !important}.sd-mb-auto,.sd-my-auto{margin-bottom:auto !important}.sd-ml-auto,.sd-mx-auto{margin-left:auto !important}.sd-m-0{margin:0 !important}.sd-mt-0,.sd-my-0{margin-top:0 !important}.sd-mr-0,.sd-mx-0{margin-right:0 !important}.sd-mb-0,.sd-my-0{margin-bottom:0 !important}.sd-ml-0,.sd-mx-0{margin-left:0 !important}.sd-m-1{margin:.25rem !important}.sd-mt-1,.sd-my-1{margin-top:.25rem !important}.sd-mr-1,.sd-mx-1{margin-right:.25rem !important}.sd-mb-1,.sd-my-1{margin-bottom:.25rem !important}.sd-ml-1,.sd-mx-1{margin-left:.25rem !important}.sd-m-2{margin:.5rem !important}.sd-mt-2,.sd-my-2{margin-top:.5rem !important}.sd-mr-2,.sd-mx-2{margin-right:.5rem !important}.sd-mb-2,.sd-my-2{margin-bottom:.5rem !important}.sd-ml-2,.sd-mx-2{margin-left:.5rem !important}.sd-m-3{margin:1rem !important}.sd-mt-3,.sd-my-3{margin-top:1rem !important}.sd-mr-3,.sd-mx-3{margin-right:1rem !important}.sd-mb-3,.sd-my-3{margin-bottom:1rem !important}.sd-ml-3,.sd-mx-3{margin-left:1rem !important}.sd-m-4{margin:1.5rem !important}.sd-mt-4,.sd-my-4{margin-top:1.5rem !important}.sd-mr-4,.sd-mx-4{margin-right:1.5rem !important}.sd-mb-4,.sd-my-4{margin-bottom:1.5rem !important}.sd-ml-4,.sd-mx-4{margin-left:1.5rem !important}.sd-m-5{margin:3rem !important}.sd-mt-5,.sd-my-5{margin-top:3rem !important}.sd-mr-5,.sd-mx-5{margin-right:3rem !important}.sd-mb-5,.sd-my-5{margin-bottom:3rem !important}.sd-ml-5,.sd-mx-5{margin-left:3rem !important}.sd-w-25{width:25% !important}.sd-w-50{width:50% !important}.sd-w-75{width:75% !important}.sd-w-100{width:100% !important}.sd-w-auto{width:auto !important}.sd-h-25{height:25% !important}.sd-h-50{height:50% !important}.sd-h-75{height:75% !important}.sd-h-100{height:100% !important}.sd-h-auto{height:auto !important}.sd-d-none{display:none !important}.sd-d-inline{display:inline !important}.sd-d-inline-block{display:inline-block !important}.sd-d-block{display:block !important}.sd-d-grid{display:grid !important}.sd-d-flex-row{display:-ms-flexbox !important;display:flex !important;flex-direction:row !important}.sd-d-flex-column{display:-ms-flexbox !important;display:flex !important;flex-direction:column !important}.sd-d-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}@media(min-width: 576px){.sd-d-sm-none{display:none !important}.sd-d-sm-inline{display:inline !important}.sd-d-sm-inline-block{display:inline-block !important}.sd-d-sm-block{display:block !important}.sd-d-sm-grid{display:grid !important}.sd-d-sm-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-sm-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 768px){.sd-d-md-none{display:none !important}.sd-d-md-inline{display:inline !important}.sd-d-md-inline-block{display:inline-block !important}.sd-d-md-block{display:block !important}.sd-d-md-grid{display:grid !important}.sd-d-md-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-md-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 992px){.sd-d-lg-none{display:none !important}.sd-d-lg-inline{display:inline !important}.sd-d-lg-inline-block{display:inline-block !important}.sd-d-lg-block{display:block !important}.sd-d-lg-grid{display:grid !important}.sd-d-lg-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-lg-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 1200px){.sd-d-xl-none{display:none !important}.sd-d-xl-inline{display:inline !important}.sd-d-xl-inline-block{display:inline-block !important}.sd-d-xl-block{display:block !important}.sd-d-xl-grid{display:grid !important}.sd-d-xl-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-xl-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}.sd-align-major-start{justify-content:flex-start !important}.sd-align-major-end{justify-content:flex-end !important}.sd-align-major-center{justify-content:center !important}.sd-align-major-justify{justify-content:space-between !important}.sd-align-major-spaced{justify-content:space-evenly !important}.sd-align-minor-start{align-items:flex-start !important}.sd-align-minor-end{align-items:flex-end !important}.sd-align-minor-center{align-items:center !important}.sd-align-minor-stretch{align-items:stretch !important}.sd-text-justify{text-align:justify !important}.sd-text-left{text-align:left !important}.sd-text-right{text-align:right !important}.sd-text-center{text-align:center !important}.sd-font-weight-light{font-weight:300 !important}.sd-font-weight-lighter{font-weight:lighter !important}.sd-font-weight-normal{font-weight:400 !important}.sd-font-weight-bold{font-weight:700 !important}.sd-font-weight-bolder{font-weight:bolder !important}.sd-font-italic{font-style:italic !important}.sd-text-decoration-none{text-decoration:none !important}.sd-text-lowercase{text-transform:lowercase !important}.sd-text-uppercase{text-transform:uppercase !important}.sd-text-capitalize{text-transform:capitalize !important}.sd-text-wrap{white-space:normal !important}.sd-text-nowrap{white-space:nowrap !important}.sd-text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sd-fs-1,.sd-fs-1>p{font-size:calc(1.375rem + 1.5vw) !important;line-height:unset !important}.sd-fs-2,.sd-fs-2>p{font-size:calc(1.325rem + 0.9vw) !important;line-height:unset !important}.sd-fs-3,.sd-fs-3>p{font-size:calc(1.3rem + 0.6vw) !important;line-height:unset !important}.sd-fs-4,.sd-fs-4>p{font-size:calc(1.275rem + 0.3vw) !important;line-height:unset !important}.sd-fs-5,.sd-fs-5>p{font-size:1.25rem !important;line-height:unset !important}.sd-fs-6,.sd-fs-6>p{font-size:1rem !important;line-height:unset !important}.sd-border-0{border:0 solid !important}.sd-border-top-0{border-top:0 solid !important}.sd-border-bottom-0{border-bottom:0 solid !important}.sd-border-right-0{border-right:0 solid !important}.sd-border-left-0{border-left:0 solid !important}.sd-border-1{border:1px solid !important}.sd-border-top-1{border-top:1px solid !important}.sd-border-bottom-1{border-bottom:1px solid !important}.sd-border-right-1{border-right:1px solid !important}.sd-border-left-1{border-left:1px solid !important}.sd-border-2{border:2px solid !important}.sd-border-top-2{border-top:2px solid !important}.sd-border-bottom-2{border-bottom:2px solid !important}.sd-border-right-2{border-right:2px solid !important}.sd-border-left-2{border-left:2px solid !important}.sd-border-3{border:3px solid !important}.sd-border-top-3{border-top:3px solid !important}.sd-border-bottom-3{border-bottom:3px solid !important}.sd-border-right-3{border-right:3px solid !important}.sd-border-left-3{border-left:3px solid !important}.sd-border-4{border:4px solid !important}.sd-border-top-4{border-top:4px solid !important}.sd-border-bottom-4{border-bottom:4px solid !important}.sd-border-right-4{border-right:4px solid !important}.sd-border-left-4{border-left:4px solid !important}.sd-border-5{border:5px solid !important}.sd-border-top-5{border-top:5px solid !important}.sd-border-bottom-5{border-bottom:5px solid !important}.sd-border-right-5{border-right:5px solid !important}.sd-border-left-5{border-left:5px solid !important}.sd-rounded-0{border-radius:0 !important}.sd-rounded-1{border-radius:.2rem !important}.sd-rounded-2{border-radius:.3rem !important}.sd-rounded-3{border-radius:.5rem !important}.sd-rounded-pill{border-radius:50rem !important}.sd-rounded-circle{border-radius:50% !important}.shadow-none{box-shadow:none !important}.sd-shadow-sm{box-shadow:0 .125rem .25rem var(--sd-color-shadow) !important}.sd-shadow-md{box-shadow:0 .5rem 1rem var(--sd-color-shadow) !important}.sd-shadow-lg{box-shadow:0 1rem 3rem var(--sd-color-shadow) !important}@keyframes sd-slide-from-left{0%{transform:translateX(-100%)}100%{transform:translateX(0)}}@keyframes sd-slide-from-right{0%{transform:translateX(200%)}100%{transform:translateX(0)}}@keyframes sd-grow100{0%{transform:scale(0);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50{0%{transform:scale(0.5);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50-rot20{0%{transform:scale(0.5) rotateZ(-20deg);opacity:.5}75%{transform:scale(1) rotateZ(5deg);opacity:1}95%{transform:scale(1) rotateZ(-1deg);opacity:1}100%{transform:scale(1) rotateZ(0);opacity:1}}.sd-animate-slide-from-left{animation:1s ease-out 0s 1 normal none running sd-slide-from-left}.sd-animate-slide-from-right{animation:1s ease-out 0s 1 normal none running sd-slide-from-right}.sd-animate-grow100{animation:1s ease-out 0s 1 normal none running sd-grow100}.sd-animate-grow50{animation:1s ease-out 0s 1 normal none running sd-grow50}.sd-animate-grow50-rot20{animation:1s ease-out 0s 1 normal none running sd-grow50-rot20}.sd-badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.sd-badge:empty{display:none}a.sd-badge{text-decoration:none}.sd-btn .sd-badge{position:relative;top:-1px}.sd-btn{background-color:transparent;border:1px solid transparent;border-radius:.25rem;cursor:pointer;display:inline-block;font-weight:400;font-size:1rem;line-height:1.5;padding:.375rem .75rem;text-align:center;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;vertical-align:middle;user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none}.sd-btn:hover{text-decoration:none}@media(prefers-reduced-motion: reduce){.sd-btn{transition:none}}.sd-btn-primary,.sd-btn-outline-primary:hover,.sd-btn-outline-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-primary:hover,.sd-btn-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary-highlight) !important;border-color:var(--sd-color-primary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-primary{color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary,.sd-btn-outline-secondary:hover,.sd-btn-outline-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary:hover,.sd-btn-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary-highlight) !important;border-color:var(--sd-color-secondary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-secondary{color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success,.sd-btn-outline-success:hover,.sd-btn-outline-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success:hover,.sd-btn-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success-highlight) !important;border-color:var(--sd-color-success-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-success{color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info,.sd-btn-outline-info:hover,.sd-btn-outline-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info:hover,.sd-btn-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info-highlight) !important;border-color:var(--sd-color-info-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-info{color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning,.sd-btn-outline-warning:hover,.sd-btn-outline-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning:hover,.sd-btn-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning-highlight) !important;border-color:var(--sd-color-warning-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-warning{color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger,.sd-btn-outline-danger:hover,.sd-btn-outline-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger:hover,.sd-btn-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger-highlight) !important;border-color:var(--sd-color-danger-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-danger{color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light,.sd-btn-outline-light:hover,.sd-btn-outline-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light:hover,.sd-btn-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light-highlight) !important;border-color:var(--sd-color-light-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-light{color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted,.sd-btn-outline-muted:hover,.sd-btn-outline-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted:hover,.sd-btn-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted-highlight) !important;border-color:var(--sd-color-muted-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-muted{color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark,.sd-btn-outline-dark:hover,.sd-btn-outline-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark:hover,.sd-btn-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark-highlight) !important;border-color:var(--sd-color-dark-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-dark{color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black,.sd-btn-outline-black:hover,.sd-btn-outline-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black:hover,.sd-btn-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black-highlight) !important;border-color:var(--sd-color-black-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-black{color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white,.sd-btn-outline-white:hover,.sd-btn-outline-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white:hover,.sd-btn-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white-highlight) !important;border-color:var(--sd-color-white-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-white{color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.sd-hide-link-text{font-size:0}.sd-octicon,.sd-material-icon{display:inline-block;fill:currentColor;vertical-align:middle}.sd-avatar-xs{border-radius:50%;object-fit:cover;object-position:center;width:1rem;height:1rem}.sd-avatar-sm{border-radius:50%;object-fit:cover;object-position:center;width:3rem;height:3rem}.sd-avatar-md{border-radius:50%;object-fit:cover;object-position:center;width:5rem;height:5rem}.sd-avatar-lg{border-radius:50%;object-fit:cover;object-position:center;width:7rem;height:7rem}.sd-avatar-xl{border-radius:50%;object-fit:cover;object-position:center;width:10rem;height:10rem}.sd-avatar-inherit{border-radius:50%;object-fit:cover;object-position:center;width:inherit;height:inherit}.sd-avatar-initial{border-radius:50%;object-fit:cover;object-position:center;width:initial;height:initial}.sd-card{background-clip:border-box;background-color:var(--sd-color-card-background);border:1px solid var(--sd-color-card-border);border-radius:.25rem;color:var(--sd-color-card-text);display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;position:relative;word-wrap:break-word}.sd-card>hr{margin-left:0;margin-right:0}.sd-card-hover:hover{border-color:var(--sd-color-card-border-hover);transform:scale(1.01)}.sd-card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem 1rem}.sd-card-title{margin-bottom:.5rem}.sd-card-subtitle{margin-top:-0.25rem;margin-bottom:0}.sd-card-text:last-child{margin-bottom:0}.sd-card-link:hover{text-decoration:none}.sd-card-link+.card-link{margin-left:1rem}.sd-card-header{padding:.5rem 1rem;margin-bottom:0;background-color:var(--sd-color-card-header);border-bottom:1px solid var(--sd-color-card-border)}.sd-card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.sd-card-footer{padding:.5rem 1rem;background-color:var(--sd-color-card-footer);border-top:1px solid var(--sd-color-card-border)}.sd-card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.sd-card-header-tabs{margin-right:-0.5rem;margin-bottom:-0.5rem;margin-left:-0.5rem;border-bottom:0}.sd-card-header-pills{margin-right:-0.5rem;margin-left:-0.5rem}.sd-card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom,.sd-card-img-top{width:100%}.sd-card-img,.sd-card-img-top{border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom{border-bottom-left-radius:calc(0.25rem - 1px);border-bottom-right-radius:calc(0.25rem - 1px)}.sd-cards-carousel{width:100%;display:flex;flex-wrap:nowrap;-ms-flex-direction:row;flex-direction:row;overflow-x:hidden;scroll-snap-type:x mandatory}.sd-cards-carousel.sd-show-scrollbar{overflow-x:auto}.sd-cards-carousel:hover,.sd-cards-carousel:focus{overflow-x:auto}.sd-cards-carousel>.sd-card{flex-shrink:0;scroll-snap-align:start}.sd-cards-carousel>.sd-card:not(:last-child){margin-right:3px}.sd-card-cols-1>.sd-card{width:90%}.sd-card-cols-2>.sd-card{width:45%}.sd-card-cols-3>.sd-card{width:30%}.sd-card-cols-4>.sd-card{width:22.5%}.sd-card-cols-5>.sd-card{width:18%}.sd-card-cols-6>.sd-card{width:15%}.sd-card-cols-7>.sd-card{width:12.8571428571%}.sd-card-cols-8>.sd-card{width:11.25%}.sd-card-cols-9>.sd-card{width:10%}.sd-card-cols-10>.sd-card{width:9%}.sd-card-cols-11>.sd-card{width:8.1818181818%}.sd-card-cols-12>.sd-card{width:7.5%}.sd-container,.sd-container-fluid,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container-xl{margin-left:auto;margin-right:auto;padding-left:var(--sd-gutter-x, 0.75rem);padding-right:var(--sd-gutter-x, 0.75rem);width:100%}@media(min-width: 576px){.sd-container-sm,.sd-container{max-width:540px}}@media(min-width: 768px){.sd-container-md,.sd-container-sm,.sd-container{max-width:720px}}@media(min-width: 992px){.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:960px}}@media(min-width: 1200px){.sd-container-xl,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:1140px}}.sd-row{--sd-gutter-x: 1.5rem;--sd-gutter-y: 0;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-top:calc(var(--sd-gutter-y) * -1);margin-right:calc(var(--sd-gutter-x) * -0.5);margin-left:calc(var(--sd-gutter-x) * -0.5)}.sd-row>*{box-sizing:border-box;flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--sd-gutter-x) * 0.5);padding-left:calc(var(--sd-gutter-x) * 0.5);margin-top:var(--sd-gutter-y)}.sd-col{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-auto>*{flex:0 0 auto;width:auto}.sd-row-cols-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}@media(min-width: 576px){.sd-col-sm{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-sm-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-sm-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-sm-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-sm-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-sm-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-sm-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-sm-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-sm-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-sm-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-sm-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-sm-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-sm-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-sm-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 768px){.sd-col-md{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-md-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-md-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-md-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-md-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-md-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-md-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-md-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-md-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-md-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-md-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-md-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-md-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-md-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 992px){.sd-col-lg{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-lg-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-lg-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-lg-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-lg-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-lg-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-lg-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-lg-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-lg-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-lg-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-lg-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-lg-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-lg-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-lg-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 1200px){.sd-col-xl{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-xl-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-xl-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-xl-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-xl-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-xl-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-xl-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-xl-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-xl-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-xl-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-xl-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-xl-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-xl-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-xl-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}.sd-col-auto{flex:0 0 auto;-ms-flex:0 0 auto;width:auto}.sd-col-1{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}.sd-col-2{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-col-3{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-col-4{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-col-5{flex:0 0 auto;-ms-flex:0 0 auto;width:41.6666666667%}.sd-col-6{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-col-7{flex:0 0 auto;-ms-flex:0 0 auto;width:58.3333333333%}.sd-col-8{flex:0 0 auto;-ms-flex:0 0 auto;width:66.6666666667%}.sd-col-9{flex:0 0 auto;-ms-flex:0 0 auto;width:75%}.sd-col-10{flex:0 0 auto;-ms-flex:0 0 auto;width:83.3333333333%}.sd-col-11{flex:0 0 auto;-ms-flex:0 0 auto;width:91.6666666667%}.sd-col-12{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-g-0,.sd-gy-0{--sd-gutter-y: 0}.sd-g-0,.sd-gx-0{--sd-gutter-x: 0}.sd-g-1,.sd-gy-1{--sd-gutter-y: 0.25rem}.sd-g-1,.sd-gx-1{--sd-gutter-x: 0.25rem}.sd-g-2,.sd-gy-2{--sd-gutter-y: 0.5rem}.sd-g-2,.sd-gx-2{--sd-gutter-x: 0.5rem}.sd-g-3,.sd-gy-3{--sd-gutter-y: 1rem}.sd-g-3,.sd-gx-3{--sd-gutter-x: 1rem}.sd-g-4,.sd-gy-4{--sd-gutter-y: 1.5rem}.sd-g-4,.sd-gx-4{--sd-gutter-x: 1.5rem}.sd-g-5,.sd-gy-5{--sd-gutter-y: 3rem}.sd-g-5,.sd-gx-5{--sd-gutter-x: 3rem}@media(min-width: 576px){.sd-col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-sm-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-sm-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-sm-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-sm-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-sm-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-sm-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-sm-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-sm-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-sm-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-sm-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-sm-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-sm-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-sm-0,.sd-gy-sm-0{--sd-gutter-y: 0}.sd-g-sm-0,.sd-gx-sm-0{--sd-gutter-x: 0}.sd-g-sm-1,.sd-gy-sm-1{--sd-gutter-y: 0.25rem}.sd-g-sm-1,.sd-gx-sm-1{--sd-gutter-x: 0.25rem}.sd-g-sm-2,.sd-gy-sm-2{--sd-gutter-y: 0.5rem}.sd-g-sm-2,.sd-gx-sm-2{--sd-gutter-x: 0.5rem}.sd-g-sm-3,.sd-gy-sm-3{--sd-gutter-y: 1rem}.sd-g-sm-3,.sd-gx-sm-3{--sd-gutter-x: 1rem}.sd-g-sm-4,.sd-gy-sm-4{--sd-gutter-y: 1.5rem}.sd-g-sm-4,.sd-gx-sm-4{--sd-gutter-x: 1.5rem}.sd-g-sm-5,.sd-gy-sm-5{--sd-gutter-y: 3rem}.sd-g-sm-5,.sd-gx-sm-5{--sd-gutter-x: 3rem}}@media(min-width: 768px){.sd-col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-md-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-md-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-md-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-md-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-md-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-md-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-md-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-md-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-md-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-md-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-md-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-md-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-md-0,.sd-gy-md-0{--sd-gutter-y: 0}.sd-g-md-0,.sd-gx-md-0{--sd-gutter-x: 0}.sd-g-md-1,.sd-gy-md-1{--sd-gutter-y: 0.25rem}.sd-g-md-1,.sd-gx-md-1{--sd-gutter-x: 0.25rem}.sd-g-md-2,.sd-gy-md-2{--sd-gutter-y: 0.5rem}.sd-g-md-2,.sd-gx-md-2{--sd-gutter-x: 0.5rem}.sd-g-md-3,.sd-gy-md-3{--sd-gutter-y: 1rem}.sd-g-md-3,.sd-gx-md-3{--sd-gutter-x: 1rem}.sd-g-md-4,.sd-gy-md-4{--sd-gutter-y: 1.5rem}.sd-g-md-4,.sd-gx-md-4{--sd-gutter-x: 1.5rem}.sd-g-md-5,.sd-gy-md-5{--sd-gutter-y: 3rem}.sd-g-md-5,.sd-gx-md-5{--sd-gutter-x: 3rem}}@media(min-width: 992px){.sd-col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-lg-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-lg-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-lg-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-lg-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-lg-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-lg-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-lg-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-lg-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-lg-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-lg-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-lg-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-lg-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-lg-0,.sd-gy-lg-0{--sd-gutter-y: 0}.sd-g-lg-0,.sd-gx-lg-0{--sd-gutter-x: 0}.sd-g-lg-1,.sd-gy-lg-1{--sd-gutter-y: 0.25rem}.sd-g-lg-1,.sd-gx-lg-1{--sd-gutter-x: 0.25rem}.sd-g-lg-2,.sd-gy-lg-2{--sd-gutter-y: 0.5rem}.sd-g-lg-2,.sd-gx-lg-2{--sd-gutter-x: 0.5rem}.sd-g-lg-3,.sd-gy-lg-3{--sd-gutter-y: 1rem}.sd-g-lg-3,.sd-gx-lg-3{--sd-gutter-x: 1rem}.sd-g-lg-4,.sd-gy-lg-4{--sd-gutter-y: 1.5rem}.sd-g-lg-4,.sd-gx-lg-4{--sd-gutter-x: 1.5rem}.sd-g-lg-5,.sd-gy-lg-5{--sd-gutter-y: 3rem}.sd-g-lg-5,.sd-gx-lg-5{--sd-gutter-x: 3rem}}@media(min-width: 1200px){.sd-col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-xl-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-xl-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-xl-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-xl-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-xl-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-xl-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-xl-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-xl-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-xl-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-xl-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-xl-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-xl-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-xl-0,.sd-gy-xl-0{--sd-gutter-y: 0}.sd-g-xl-0,.sd-gx-xl-0{--sd-gutter-x: 0}.sd-g-xl-1,.sd-gy-xl-1{--sd-gutter-y: 0.25rem}.sd-g-xl-1,.sd-gx-xl-1{--sd-gutter-x: 0.25rem}.sd-g-xl-2,.sd-gy-xl-2{--sd-gutter-y: 0.5rem}.sd-g-xl-2,.sd-gx-xl-2{--sd-gutter-x: 0.5rem}.sd-g-xl-3,.sd-gy-xl-3{--sd-gutter-y: 1rem}.sd-g-xl-3,.sd-gx-xl-3{--sd-gutter-x: 1rem}.sd-g-xl-4,.sd-gy-xl-4{--sd-gutter-y: 1.5rem}.sd-g-xl-4,.sd-gx-xl-4{--sd-gutter-x: 1.5rem}.sd-g-xl-5,.sd-gy-xl-5{--sd-gutter-y: 3rem}.sd-g-xl-5,.sd-gx-xl-5{--sd-gutter-x: 3rem}}.sd-flex-row-reverse{flex-direction:row-reverse !important}details.sd-dropdown{position:relative}details.sd-dropdown .sd-summary-title{font-weight:700;padding-right:3em !important;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none}details.sd-dropdown:hover{cursor:pointer}details.sd-dropdown .sd-summary-content{cursor:default}details.sd-dropdown summary{list-style:none;padding:1em}details.sd-dropdown summary .sd-octicon.no-title{vertical-align:middle}details.sd-dropdown[open] summary .sd-octicon.no-title{visibility:hidden}details.sd-dropdown summary::-webkit-details-marker{display:none}details.sd-dropdown summary:focus{outline:none}details.sd-dropdown .sd-summary-icon{margin-right:.5em}details.sd-dropdown .sd-summary-icon svg{opacity:.8}details.sd-dropdown summary:hover .sd-summary-up svg,details.sd-dropdown summary:hover .sd-summary-down svg{opacity:1;transform:scale(1.1)}details.sd-dropdown .sd-summary-up svg,details.sd-dropdown .sd-summary-down svg{display:block;opacity:.6}details.sd-dropdown .sd-summary-up,details.sd-dropdown .sd-summary-down{pointer-events:none;position:absolute;right:1em;top:1em}details.sd-dropdown[open]>.sd-summary-title .sd-summary-down{visibility:hidden}details.sd-dropdown:not([open])>.sd-summary-title .sd-summary-up{visibility:hidden}details.sd-dropdown:not([open]).sd-card{border:none}details.sd-dropdown:not([open])>.sd-card-header{border:1px solid var(--sd-color-card-border);border-radius:.25rem}details.sd-dropdown.sd-fade-in[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out;animation:sd-fade-in .5s ease-in-out}details.sd-dropdown.sd-fade-in-slide-down[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out}.sd-col>.sd-dropdown{width:100%}.sd-summary-content>.sd-tab-set:first-child{margin-top:0}@keyframes sd-fade-in{0%{opacity:0}100%{opacity:1}}@keyframes sd-slide-down{0%{transform:translate(0, -10px)}100%{transform:translate(0, 0)}}.sd-tab-set{border-radius:.125rem;display:flex;flex-wrap:wrap;margin:1em 0;position:relative}.sd-tab-set>input{opacity:0;position:absolute}.sd-tab-set>input:checked+label{border-color:var(--sd-color-tabs-underline-active);color:var(--sd-color-tabs-label-active)}.sd-tab-set>input:checked+label+.sd-tab-content{display:block}.sd-tab-set>input:not(:checked)+label:hover{color:var(--sd-color-tabs-label-hover);border-color:var(--sd-color-tabs-underline-hover)}.sd-tab-set>input:focus+label{outline-style:auto}.sd-tab-set>input:not(.focus-visible)+label{outline:none;-webkit-tap-highlight-color:transparent}.sd-tab-set>label{border-bottom:.125rem solid transparent;margin-bottom:0;color:var(--sd-color-tabs-label-inactive);border-color:var(--sd-color-tabs-underline-inactive);cursor:pointer;font-size:var(--sd-fontsize-tabs-label);font-weight:700;padding:1em 1.25em .5em;transition:color 250ms;width:auto;z-index:1}html .sd-tab-set>label:hover{color:var(--sd-color-tabs-label-active)}.sd-col>.sd-tab-set{width:100%}.sd-tab-content{box-shadow:0 -0.0625rem var(--sd-color-tabs-overline),0 .0625rem var(--sd-color-tabs-underline);display:none;order:99;padding-bottom:.75rem;padding-top:.75rem;width:100%}.sd-tab-content>:first-child{margin-top:0 !important}.sd-tab-content>:last-child{margin-bottom:0 !important}.sd-tab-content>.sd-tab-set{margin:0}.sd-sphinx-override,.sd-sphinx-override *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.sd-sphinx-override p{margin-top:0}:root{--sd-color-primary: #0071bc;--sd-color-secondary: #6c757d;--sd-color-success: #28a745;--sd-color-info: #17a2b8;--sd-color-warning: #f0b37e;--sd-color-danger: #dc3545;--sd-color-light: #f8f9fa;--sd-color-muted: #6c757d;--sd-color-dark: #212529;--sd-color-black: black;--sd-color-white: white;--sd-color-primary-highlight: #0060a0;--sd-color-secondary-highlight: #5c636a;--sd-color-success-highlight: #228e3b;--sd-color-info-highlight: #148a9c;--sd-color-warning-highlight: #cc986b;--sd-color-danger-highlight: #bb2d3b;--sd-color-light-highlight: #d3d4d5;--sd-color-muted-highlight: #5c636a;--sd-color-dark-highlight: #1c1f23;--sd-color-black-highlight: black;--sd-color-white-highlight: #d9d9d9;--sd-color-primary-text: #fff;--sd-color-secondary-text: #fff;--sd-color-success-text: #fff;--sd-color-info-text: #fff;--sd-color-warning-text: #212529;--sd-color-danger-text: #fff;--sd-color-light-text: #212529;--sd-color-muted-text: #fff;--sd-color-dark-text: #fff;--sd-color-black-text: #fff;--sd-color-white-text: #212529;--sd-color-shadow: rgba(0, 0, 0, 0.15);--sd-color-card-border: rgba(0, 0, 0, 0.125);--sd-color-card-border-hover: hsla(231, 99%, 66%, 1);--sd-color-card-background: transparent;--sd-color-card-text: inherit;--sd-color-card-header: transparent;--sd-color-card-footer: transparent;--sd-color-tabs-label-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-hover: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-inactive: hsl(0, 0%, 66%);--sd-color-tabs-underline-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-underline-hover: rgba(178, 206, 245, 0.62);--sd-color-tabs-underline-inactive: transparent;--sd-color-tabs-overline: rgb(222, 222, 222);--sd-color-tabs-underline: rgb(222, 222, 222);--sd-fontsize-tabs-label: 1rem} diff --git a/main/_sphinx_design_static/design-tabs.js b/main/_sphinx_design_static/design-tabs.js new file mode 100644 index 0000000..36b38cf --- /dev/null +++ b/main/_sphinx_design_static/design-tabs.js @@ -0,0 +1,27 @@ +var sd_labels_by_text = {}; + +function ready() { + const li = document.getElementsByClassName("sd-tab-label"); + for (const label of li) { + syncId = label.getAttribute("data-sync-id"); + if (syncId) { + label.onclick = onLabelClick; + if (!sd_labels_by_text[syncId]) { + sd_labels_by_text[syncId] = []; + } + sd_labels_by_text[syncId].push(label); + } + } +} + +function onLabelClick() { + // Activate other inputs with the same sync id. + syncId = this.getAttribute("data-sync-id"); + for (label of sd_labels_by_text[syncId]) { + if (label === this) continue; + label.previousElementSibling.checked = true; + } + window.localStorage.setItem("sphinx-design-last-tab", syncId); +} + +document.addEventListener("DOMContentLoaded", ready, false); diff --git a/main/_static/AuWorldEQ.jpg b/main/_static/AuWorldEQ.jpg new file mode 100644 index 0000000..f807ccd Binary files /dev/null and b/main/_static/AuWorldEQ.jpg differ diff --git a/main/_static/basic.css b/main/_static/basic.css new file mode 100644 index 0000000..e760386 --- /dev/null +++ b/main/_static/basic.css @@ -0,0 +1,925 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 270px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 360px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +a:visited { + color: #551A8B; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.sig dd { + margin-top: 0px; + margin-bottom: 0px; +} + +.sig dl { + margin-top: 0px; + margin-bottom: 0px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +.translated { + background-color: rgba(207, 255, 207, 0.2) +} + +.untranslated { + background-color: rgba(255, 207, 207, 0.2) +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/main/_static/check-solid.svg b/main/_static/check-solid.svg new file mode 100644 index 0000000..92fad4b --- /dev/null +++ b/main/_static/check-solid.svg @@ -0,0 +1,4 @@ + + + + diff --git a/main/_static/clipboard.min.js b/main/_static/clipboard.min.js new file mode 100644 index 0000000..54b3c46 --- /dev/null +++ b/main/_static/clipboard.min.js @@ -0,0 +1,7 @@ +/*! + * clipboard.js v2.0.8 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return n={686:function(t,e,n){"use strict";n.d(e,{default:function(){return o}});var e=n(279),i=n.n(e),e=n(370),u=n.n(e),e=n(817),c=n.n(e);function a(t){try{return document.execCommand(t)}catch(t){return}}var f=function(t){t=c()(t);return a("cut"),t};var l=function(t){var e,n,o,r=1 + + + + diff --git a/main/_static/copybutton.css b/main/_static/copybutton.css new file mode 100644 index 0000000..f1916ec --- /dev/null +++ b/main/_static/copybutton.css @@ -0,0 +1,94 @@ +/* Copy buttons */ +button.copybtn { + position: absolute; + display: flex; + top: .3em; + right: .3em; + width: 1.7em; + height: 1.7em; + opacity: 0; + transition: opacity 0.3s, border .3s, background-color .3s; + user-select: none; + padding: 0; + border: none; + outline: none; + border-radius: 0.4em; + /* The colors that GitHub uses */ + border: #1b1f2426 1px solid; + background-color: #f6f8fa; + color: #57606a; +} + +button.copybtn.success { + border-color: #22863a; + color: #22863a; +} + +button.copybtn svg { + stroke: currentColor; + width: 1.5em; + height: 1.5em; + padding: 0.1em; +} + +div.highlight { + position: relative; +} + +/* Show the copybutton */ +.highlight:hover button.copybtn, button.copybtn.success { + opacity: 1; +} + +.highlight button.copybtn:hover { + background-color: rgb(235, 235, 235); +} + +.highlight button.copybtn:active { + background-color: rgb(187, 187, 187); +} + +/** + * A minimal CSS-only tooltip copied from: + * https://codepen.io/mildrenben/pen/rVBrpK + * + * To use, write HTML like the following: + * + *

Short

+ */ + .o-tooltip--left { + position: relative; + } + + .o-tooltip--left:after { + opacity: 0; + visibility: hidden; + position: absolute; + content: attr(data-tooltip); + padding: .2em; + font-size: .8em; + left: -.2em; + background: grey; + color: white; + white-space: nowrap; + z-index: 2; + border-radius: 2px; + transform: translateX(-102%) translateY(0); + transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); +} + +.o-tooltip--left:hover:after { + display: block; + opacity: 1; + visibility: visible; + transform: translateX(-100%) translateY(0); + transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); + transition-delay: .5s; +} + +/* By default the copy button shouldn't show up when printing a page */ +@media print { + button.copybtn { + display: none; + } +} diff --git a/main/_static/copybutton.js b/main/_static/copybutton.js new file mode 100644 index 0000000..2ea7ff3 --- /dev/null +++ b/main/_static/copybutton.js @@ -0,0 +1,248 @@ +// Localization support +const messages = { + 'en': { + 'copy': 'Copy', + 'copy_to_clipboard': 'Copy to clipboard', + 'copy_success': 'Copied!', + 'copy_failure': 'Failed to copy', + }, + 'es' : { + 'copy': 'Copiar', + 'copy_to_clipboard': 'Copiar al portapapeles', + 'copy_success': '¡Copiado!', + 'copy_failure': 'Error al copiar', + }, + 'de' : { + 'copy': 'Kopieren', + 'copy_to_clipboard': 'In die Zwischenablage kopieren', + 'copy_success': 'Kopiert!', + 'copy_failure': 'Fehler beim Kopieren', + }, + 'fr' : { + 'copy': 'Copier', + 'copy_to_clipboard': 'Copier dans le presse-papier', + 'copy_success': 'Copié !', + 'copy_failure': 'Échec de la copie', + }, + 'ru': { + 'copy': 'Скопировать', + 'copy_to_clipboard': 'Скопировать в буфер', + 'copy_success': 'Скопировано!', + 'copy_failure': 'Не удалось скопировать', + }, + 'zh-CN': { + 'copy': '复制', + 'copy_to_clipboard': '复制到剪贴板', + 'copy_success': '复制成功!', + 'copy_failure': '复制失败', + }, + 'it' : { + 'copy': 'Copiare', + 'copy_to_clipboard': 'Copiato negli appunti', + 'copy_success': 'Copiato!', + 'copy_failure': 'Errore durante la copia', + } +} + +let locale = 'en' +if( document.documentElement.lang !== undefined + && messages[document.documentElement.lang] !== undefined ) { + locale = document.documentElement.lang +} + +let doc_url_root = DOCUMENTATION_OPTIONS.URL_ROOT; +if (doc_url_root == '#') { + doc_url_root = ''; +} + +/** + * SVG files for our copy buttons + */ +let iconCheck = ` + ${messages[locale]['copy_success']} + + +` + +// If the user specified their own SVG use that, otherwise use the default +let iconCopy = ``; +if (!iconCopy) { + iconCopy = ` + ${messages[locale]['copy_to_clipboard']} + + + +` +} + +/** + * Set up copy/paste for code blocks + */ + +const runWhenDOMLoaded = cb => { + if (document.readyState != 'loading') { + cb() + } else if (document.addEventListener) { + document.addEventListener('DOMContentLoaded', cb) + } else { + document.attachEvent('onreadystatechange', function() { + if (document.readyState == 'complete') cb() + }) + } +} + +const codeCellId = index => `codecell${index}` + +// Clears selected text since ClipboardJS will select the text when copying +const clearSelection = () => { + if (window.getSelection) { + window.getSelection().removeAllRanges() + } else if (document.selection) { + document.selection.empty() + } +} + +// Changes tooltip text for a moment, then changes it back +// We want the timeout of our `success` class to be a bit shorter than the +// tooltip and icon change, so that we can hide the icon before changing back. +var timeoutIcon = 2000; +var timeoutSuccessClass = 1500; + +const temporarilyChangeTooltip = (el, oldText, newText) => { + el.setAttribute('data-tooltip', newText) + el.classList.add('success') + // Remove success a little bit sooner than we change the tooltip + // So that we can use CSS to hide the copybutton first + setTimeout(() => el.classList.remove('success'), timeoutSuccessClass) + setTimeout(() => el.setAttribute('data-tooltip', oldText), timeoutIcon) +} + +// Changes the copy button icon for two seconds, then changes it back +const temporarilyChangeIcon = (el) => { + el.innerHTML = iconCheck; + setTimeout(() => {el.innerHTML = iconCopy}, timeoutIcon) +} + +const addCopyButtonToCodeCells = () => { + // If ClipboardJS hasn't loaded, wait a bit and try again. This + // happens because we load ClipboardJS asynchronously. + if (window.ClipboardJS === undefined) { + setTimeout(addCopyButtonToCodeCells, 250) + return + } + + // Add copybuttons to all of our code cells + const COPYBUTTON_SELECTOR = 'div.highlight pre'; + const codeCells = document.querySelectorAll(COPYBUTTON_SELECTOR) + codeCells.forEach((codeCell, index) => { + const id = codeCellId(index) + codeCell.setAttribute('id', id) + + const clipboardButton = id => + `` + codeCell.insertAdjacentHTML('afterend', clipboardButton(id)) + }) + +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} + + +var copyTargetText = (trigger) => { + var target = document.querySelector(trigger.attributes['data-clipboard-target'].value); + + // get filtered text + let exclude = '.linenos'; + + let text = filterText(target, exclude); + return formatCopyText(text, '', false, true, true, true, '', '') +} + + // Initialize with a callback so we can modify the text before copy + const clipboard = new ClipboardJS('.copybtn', {text: copyTargetText}) + + // Update UI with error/success messages + clipboard.on('success', event => { + clearSelection() + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_success']) + temporarilyChangeIcon(event.trigger) + }) + + clipboard.on('error', event => { + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_failure']) + }) +} + +runWhenDOMLoaded(addCopyButtonToCodeCells) \ No newline at end of file diff --git a/main/_static/copybutton_funcs.js b/main/_static/copybutton_funcs.js new file mode 100644 index 0000000..dbe1aaa --- /dev/null +++ b/main/_static/copybutton_funcs.js @@ -0,0 +1,73 @@ +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +export function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +export function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} diff --git a/main/_static/customize.css b/main/_static/customize.css new file mode 100644 index 0000000..368fb08 --- /dev/null +++ b/main/_static/customize.css @@ -0,0 +1,54 @@ +/* Fonts: "Open Sans", "Roboto", "Yanone Kaffeesatz" */ +@import url("https://indestructibletype.com/fonts/Jost.css"); +@import url('https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible&family=Open+Sans:wght@300&family=Oswald:wght@300&family=Roboto:wght@300&family=Yanone+Kaffeesatz:wght@300&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Raleway:wght@300&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Cinzel&family=Cormorant:wght@300&family=Lobster+Two&family=Noticia+Text&display=swap'); + +/* Options for root variables: + https://github.com/pydata/pydata-sphinx-theme/blob/master/pydata_sphinx_theme/static/css/theme.css */ + + +:root { + --pst-font-family-legible: "Atkinson Hyperlegible", "Roboto", sans-serif; + --pst-font-family-legible-headers: var(--pst-font-family-legible); + --pst-font-family-pretty: "Jost", "Roboto", sans-serif; + --pst-font-family-pretty-headers: "Raleway", "Jost", "Roboto", sans-serif; + + /* Set them */ + --pst-font-family-base: var(--pst-font-family-pretty); + --pst-font-family-heading: var(--pst-font-family-headers); + + /* In the original .css, the font sizes are hardwired despite there being a base setting */ + + --pst-font-scale-factor: 1.0; + --pst-font-size-base: calc(10pt * var(--pst-font-scale-factor)); /* base font size - applied at body / html level */ + + /* heading font sizes */ + --pst-font-size-h1: calc( var(--pst-font-size-base) * 2.1); + --pst-font-size-h2: calc( var(--pst-font-size-base) * 1.9); + --pst-font-size-h3: calc( var(--pst-font-size-base) * 1.5); + --pst-font-size-h4: calc( var(--pst-font-size-base) * 1.3); + --pst-font-size-h5: calc( var(--pst-font-size-base) * 1.2); + --pst-font-size-h6: calc( var(--pst-font-size-base) * 1.1); + +} + +p { + text-align: justify; +} + + +/* This is to drop elements from the navigation when printing. */ +@media print { + .bd-toc { + visibility: hidden; + width: 0px; + } + + footer { + visibility: hidden; + } + +} + + diff --git a/main/_static/design-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css b/main/_static/design-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css new file mode 100644 index 0000000..eb19f69 --- /dev/null +++ b/main/_static/design-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css @@ -0,0 +1 @@ +.sd-bg-primary{background-color:var(--sd-color-primary) !important}.sd-bg-text-primary{color:var(--sd-color-primary-text) !important}button.sd-bg-primary:focus,button.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}a.sd-bg-primary:focus,a.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}.sd-bg-secondary{background-color:var(--sd-color-secondary) !important}.sd-bg-text-secondary{color:var(--sd-color-secondary-text) !important}button.sd-bg-secondary:focus,button.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}a.sd-bg-secondary:focus,a.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}.sd-bg-success{background-color:var(--sd-color-success) !important}.sd-bg-text-success{color:var(--sd-color-success-text) !important}button.sd-bg-success:focus,button.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}a.sd-bg-success:focus,a.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}.sd-bg-info{background-color:var(--sd-color-info) !important}.sd-bg-text-info{color:var(--sd-color-info-text) !important}button.sd-bg-info:focus,button.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}a.sd-bg-info:focus,a.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}.sd-bg-warning{background-color:var(--sd-color-warning) !important}.sd-bg-text-warning{color:var(--sd-color-warning-text) !important}button.sd-bg-warning:focus,button.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}a.sd-bg-warning:focus,a.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}.sd-bg-danger{background-color:var(--sd-color-danger) !important}.sd-bg-text-danger{color:var(--sd-color-danger-text) !important}button.sd-bg-danger:focus,button.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}a.sd-bg-danger:focus,a.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}.sd-bg-light{background-color:var(--sd-color-light) !important}.sd-bg-text-light{color:var(--sd-color-light-text) !important}button.sd-bg-light:focus,button.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}a.sd-bg-light:focus,a.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}.sd-bg-muted{background-color:var(--sd-color-muted) !important}.sd-bg-text-muted{color:var(--sd-color-muted-text) !important}button.sd-bg-muted:focus,button.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}a.sd-bg-muted:focus,a.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}.sd-bg-dark{background-color:var(--sd-color-dark) !important}.sd-bg-text-dark{color:var(--sd-color-dark-text) !important}button.sd-bg-dark:focus,button.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}a.sd-bg-dark:focus,a.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}.sd-bg-black{background-color:var(--sd-color-black) !important}.sd-bg-text-black{color:var(--sd-color-black-text) !important}button.sd-bg-black:focus,button.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}a.sd-bg-black:focus,a.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}.sd-bg-white{background-color:var(--sd-color-white) !important}.sd-bg-text-white{color:var(--sd-color-white-text) !important}button.sd-bg-white:focus,button.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}a.sd-bg-white:focus,a.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}.sd-text-primary,.sd-text-primary>p{color:var(--sd-color-primary) !important}a.sd-text-primary:focus,a.sd-text-primary:hover{color:var(--sd-color-primary-highlight) !important}.sd-text-secondary,.sd-text-secondary>p{color:var(--sd-color-secondary) !important}a.sd-text-secondary:focus,a.sd-text-secondary:hover{color:var(--sd-color-secondary-highlight) !important}.sd-text-success,.sd-text-success>p{color:var(--sd-color-success) !important}a.sd-text-success:focus,a.sd-text-success:hover{color:var(--sd-color-success-highlight) !important}.sd-text-info,.sd-text-info>p{color:var(--sd-color-info) !important}a.sd-text-info:focus,a.sd-text-info:hover{color:var(--sd-color-info-highlight) !important}.sd-text-warning,.sd-text-warning>p{color:var(--sd-color-warning) !important}a.sd-text-warning:focus,a.sd-text-warning:hover{color:var(--sd-color-warning-highlight) !important}.sd-text-danger,.sd-text-danger>p{color:var(--sd-color-danger) !important}a.sd-text-danger:focus,a.sd-text-danger:hover{color:var(--sd-color-danger-highlight) !important}.sd-text-light,.sd-text-light>p{color:var(--sd-color-light) !important}a.sd-text-light:focus,a.sd-text-light:hover{color:var(--sd-color-light-highlight) !important}.sd-text-muted,.sd-text-muted>p{color:var(--sd-color-muted) !important}a.sd-text-muted:focus,a.sd-text-muted:hover{color:var(--sd-color-muted-highlight) !important}.sd-text-dark,.sd-text-dark>p{color:var(--sd-color-dark) !important}a.sd-text-dark:focus,a.sd-text-dark:hover{color:var(--sd-color-dark-highlight) !important}.sd-text-black,.sd-text-black>p{color:var(--sd-color-black) !important}a.sd-text-black:focus,a.sd-text-black:hover{color:var(--sd-color-black-highlight) !important}.sd-text-white,.sd-text-white>p{color:var(--sd-color-white) !important}a.sd-text-white:focus,a.sd-text-white:hover{color:var(--sd-color-white-highlight) !important}.sd-outline-primary{border-color:var(--sd-color-primary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-primary:focus,a.sd-outline-primary:hover{border-color:var(--sd-color-primary-highlight) !important}.sd-outline-secondary{border-color:var(--sd-color-secondary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-secondary:focus,a.sd-outline-secondary:hover{border-color:var(--sd-color-secondary-highlight) !important}.sd-outline-success{border-color:var(--sd-color-success) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-success:focus,a.sd-outline-success:hover{border-color:var(--sd-color-success-highlight) !important}.sd-outline-info{border-color:var(--sd-color-info) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-info:focus,a.sd-outline-info:hover{border-color:var(--sd-color-info-highlight) !important}.sd-outline-warning{border-color:var(--sd-color-warning) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-warning:focus,a.sd-outline-warning:hover{border-color:var(--sd-color-warning-highlight) !important}.sd-outline-danger{border-color:var(--sd-color-danger) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-danger:focus,a.sd-outline-danger:hover{border-color:var(--sd-color-danger-highlight) !important}.sd-outline-light{border-color:var(--sd-color-light) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-light:focus,a.sd-outline-light:hover{border-color:var(--sd-color-light-highlight) !important}.sd-outline-muted{border-color:var(--sd-color-muted) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-muted:focus,a.sd-outline-muted:hover{border-color:var(--sd-color-muted-highlight) !important}.sd-outline-dark{border-color:var(--sd-color-dark) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-dark:focus,a.sd-outline-dark:hover{border-color:var(--sd-color-dark-highlight) !important}.sd-outline-black{border-color:var(--sd-color-black) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-black:focus,a.sd-outline-black:hover{border-color:var(--sd-color-black-highlight) !important}.sd-outline-white{border-color:var(--sd-color-white) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-white:focus,a.sd-outline-white:hover{border-color:var(--sd-color-white-highlight) !important}.sd-bg-transparent{background-color:transparent !important}.sd-outline-transparent{border-color:transparent !important}.sd-text-transparent{color:transparent !important}.sd-p-0{padding:0 !important}.sd-pt-0,.sd-py-0{padding-top:0 !important}.sd-pr-0,.sd-px-0{padding-right:0 !important}.sd-pb-0,.sd-py-0{padding-bottom:0 !important}.sd-pl-0,.sd-px-0{padding-left:0 !important}.sd-p-1{padding:.25rem !important}.sd-pt-1,.sd-py-1{padding-top:.25rem !important}.sd-pr-1,.sd-px-1{padding-right:.25rem !important}.sd-pb-1,.sd-py-1{padding-bottom:.25rem !important}.sd-pl-1,.sd-px-1{padding-left:.25rem !important}.sd-p-2{padding:.5rem !important}.sd-pt-2,.sd-py-2{padding-top:.5rem !important}.sd-pr-2,.sd-px-2{padding-right:.5rem !important}.sd-pb-2,.sd-py-2{padding-bottom:.5rem !important}.sd-pl-2,.sd-px-2{padding-left:.5rem !important}.sd-p-3{padding:1rem !important}.sd-pt-3,.sd-py-3{padding-top:1rem !important}.sd-pr-3,.sd-px-3{padding-right:1rem !important}.sd-pb-3,.sd-py-3{padding-bottom:1rem !important}.sd-pl-3,.sd-px-3{padding-left:1rem !important}.sd-p-4{padding:1.5rem !important}.sd-pt-4,.sd-py-4{padding-top:1.5rem !important}.sd-pr-4,.sd-px-4{padding-right:1.5rem !important}.sd-pb-4,.sd-py-4{padding-bottom:1.5rem !important}.sd-pl-4,.sd-px-4{padding-left:1.5rem !important}.sd-p-5{padding:3rem !important}.sd-pt-5,.sd-py-5{padding-top:3rem !important}.sd-pr-5,.sd-px-5{padding-right:3rem !important}.sd-pb-5,.sd-py-5{padding-bottom:3rem !important}.sd-pl-5,.sd-px-5{padding-left:3rem !important}.sd-m-auto{margin:auto !important}.sd-mt-auto,.sd-my-auto{margin-top:auto !important}.sd-mr-auto,.sd-mx-auto{margin-right:auto !important}.sd-mb-auto,.sd-my-auto{margin-bottom:auto !important}.sd-ml-auto,.sd-mx-auto{margin-left:auto !important}.sd-m-0{margin:0 !important}.sd-mt-0,.sd-my-0{margin-top:0 !important}.sd-mr-0,.sd-mx-0{margin-right:0 !important}.sd-mb-0,.sd-my-0{margin-bottom:0 !important}.sd-ml-0,.sd-mx-0{margin-left:0 !important}.sd-m-1{margin:.25rem !important}.sd-mt-1,.sd-my-1{margin-top:.25rem !important}.sd-mr-1,.sd-mx-1{margin-right:.25rem !important}.sd-mb-1,.sd-my-1{margin-bottom:.25rem !important}.sd-ml-1,.sd-mx-1{margin-left:.25rem !important}.sd-m-2{margin:.5rem !important}.sd-mt-2,.sd-my-2{margin-top:.5rem !important}.sd-mr-2,.sd-mx-2{margin-right:.5rem !important}.sd-mb-2,.sd-my-2{margin-bottom:.5rem !important}.sd-ml-2,.sd-mx-2{margin-left:.5rem !important}.sd-m-3{margin:1rem !important}.sd-mt-3,.sd-my-3{margin-top:1rem !important}.sd-mr-3,.sd-mx-3{margin-right:1rem !important}.sd-mb-3,.sd-my-3{margin-bottom:1rem !important}.sd-ml-3,.sd-mx-3{margin-left:1rem !important}.sd-m-4{margin:1.5rem !important}.sd-mt-4,.sd-my-4{margin-top:1.5rem !important}.sd-mr-4,.sd-mx-4{margin-right:1.5rem !important}.sd-mb-4,.sd-my-4{margin-bottom:1.5rem !important}.sd-ml-4,.sd-mx-4{margin-left:1.5rem !important}.sd-m-5{margin:3rem !important}.sd-mt-5,.sd-my-5{margin-top:3rem !important}.sd-mr-5,.sd-mx-5{margin-right:3rem !important}.sd-mb-5,.sd-my-5{margin-bottom:3rem !important}.sd-ml-5,.sd-mx-5{margin-left:3rem !important}.sd-w-25{width:25% !important}.sd-w-50{width:50% !important}.sd-w-75{width:75% !important}.sd-w-100{width:100% !important}.sd-w-auto{width:auto !important}.sd-h-25{height:25% !important}.sd-h-50{height:50% !important}.sd-h-75{height:75% !important}.sd-h-100{height:100% !important}.sd-h-auto{height:auto !important}.sd-d-none{display:none !important}.sd-d-inline{display:inline !important}.sd-d-inline-block{display:inline-block !important}.sd-d-block{display:block !important}.sd-d-grid{display:grid !important}.sd-d-flex-row{display:-ms-flexbox !important;display:flex !important;flex-direction:row !important}.sd-d-flex-column{display:-ms-flexbox !important;display:flex !important;flex-direction:column !important}.sd-d-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}@media(min-width: 576px){.sd-d-sm-none{display:none !important}.sd-d-sm-inline{display:inline !important}.sd-d-sm-inline-block{display:inline-block !important}.sd-d-sm-block{display:block !important}.sd-d-sm-grid{display:grid !important}.sd-d-sm-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-sm-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 768px){.sd-d-md-none{display:none !important}.sd-d-md-inline{display:inline !important}.sd-d-md-inline-block{display:inline-block !important}.sd-d-md-block{display:block !important}.sd-d-md-grid{display:grid !important}.sd-d-md-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-md-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 992px){.sd-d-lg-none{display:none !important}.sd-d-lg-inline{display:inline !important}.sd-d-lg-inline-block{display:inline-block !important}.sd-d-lg-block{display:block !important}.sd-d-lg-grid{display:grid !important}.sd-d-lg-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-lg-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 1200px){.sd-d-xl-none{display:none !important}.sd-d-xl-inline{display:inline !important}.sd-d-xl-inline-block{display:inline-block !important}.sd-d-xl-block{display:block !important}.sd-d-xl-grid{display:grid !important}.sd-d-xl-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-xl-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}.sd-align-major-start{justify-content:flex-start !important}.sd-align-major-end{justify-content:flex-end !important}.sd-align-major-center{justify-content:center !important}.sd-align-major-justify{justify-content:space-between !important}.sd-align-major-spaced{justify-content:space-evenly !important}.sd-align-minor-start{align-items:flex-start !important}.sd-align-minor-end{align-items:flex-end !important}.sd-align-minor-center{align-items:center !important}.sd-align-minor-stretch{align-items:stretch !important}.sd-text-justify{text-align:justify !important}.sd-text-left{text-align:left !important}.sd-text-right{text-align:right !important}.sd-text-center{text-align:center !important}.sd-font-weight-light{font-weight:300 !important}.sd-font-weight-lighter{font-weight:lighter !important}.sd-font-weight-normal{font-weight:400 !important}.sd-font-weight-bold{font-weight:700 !important}.sd-font-weight-bolder{font-weight:bolder !important}.sd-font-italic{font-style:italic !important}.sd-text-decoration-none{text-decoration:none !important}.sd-text-lowercase{text-transform:lowercase !important}.sd-text-uppercase{text-transform:uppercase !important}.sd-text-capitalize{text-transform:capitalize !important}.sd-text-wrap{white-space:normal !important}.sd-text-nowrap{white-space:nowrap !important}.sd-text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sd-fs-1,.sd-fs-1>p{font-size:calc(1.375rem + 1.5vw) !important;line-height:unset !important}.sd-fs-2,.sd-fs-2>p{font-size:calc(1.325rem + 0.9vw) !important;line-height:unset !important}.sd-fs-3,.sd-fs-3>p{font-size:calc(1.3rem + 0.6vw) !important;line-height:unset !important}.sd-fs-4,.sd-fs-4>p{font-size:calc(1.275rem + 0.3vw) !important;line-height:unset !important}.sd-fs-5,.sd-fs-5>p{font-size:1.25rem !important;line-height:unset !important}.sd-fs-6,.sd-fs-6>p{font-size:1rem !important;line-height:unset !important}.sd-border-0{border:0 solid !important}.sd-border-top-0{border-top:0 solid !important}.sd-border-bottom-0{border-bottom:0 solid !important}.sd-border-right-0{border-right:0 solid !important}.sd-border-left-0{border-left:0 solid !important}.sd-border-1{border:1px solid !important}.sd-border-top-1{border-top:1px solid !important}.sd-border-bottom-1{border-bottom:1px solid !important}.sd-border-right-1{border-right:1px solid !important}.sd-border-left-1{border-left:1px solid !important}.sd-border-2{border:2px solid !important}.sd-border-top-2{border-top:2px solid !important}.sd-border-bottom-2{border-bottom:2px solid !important}.sd-border-right-2{border-right:2px solid !important}.sd-border-left-2{border-left:2px solid !important}.sd-border-3{border:3px solid !important}.sd-border-top-3{border-top:3px solid !important}.sd-border-bottom-3{border-bottom:3px solid !important}.sd-border-right-3{border-right:3px solid !important}.sd-border-left-3{border-left:3px solid !important}.sd-border-4{border:4px solid !important}.sd-border-top-4{border-top:4px solid !important}.sd-border-bottom-4{border-bottom:4px solid !important}.sd-border-right-4{border-right:4px solid !important}.sd-border-left-4{border-left:4px solid !important}.sd-border-5{border:5px solid !important}.sd-border-top-5{border-top:5px solid !important}.sd-border-bottom-5{border-bottom:5px solid !important}.sd-border-right-5{border-right:5px solid !important}.sd-border-left-5{border-left:5px solid !important}.sd-rounded-0{border-radius:0 !important}.sd-rounded-1{border-radius:.2rem !important}.sd-rounded-2{border-radius:.3rem !important}.sd-rounded-3{border-radius:.5rem !important}.sd-rounded-pill{border-radius:50rem !important}.sd-rounded-circle{border-radius:50% !important}.shadow-none{box-shadow:none !important}.sd-shadow-sm{box-shadow:0 .125rem .25rem var(--sd-color-shadow) !important}.sd-shadow-md{box-shadow:0 .5rem 1rem var(--sd-color-shadow) !important}.sd-shadow-lg{box-shadow:0 1rem 3rem var(--sd-color-shadow) !important}@keyframes sd-slide-from-left{0%{transform:translateX(-100%)}100%{transform:translateX(0)}}@keyframes sd-slide-from-right{0%{transform:translateX(200%)}100%{transform:translateX(0)}}@keyframes sd-grow100{0%{transform:scale(0);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50{0%{transform:scale(0.5);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50-rot20{0%{transform:scale(0.5) rotateZ(-20deg);opacity:.5}75%{transform:scale(1) rotateZ(5deg);opacity:1}95%{transform:scale(1) rotateZ(-1deg);opacity:1}100%{transform:scale(1) rotateZ(0);opacity:1}}.sd-animate-slide-from-left{animation:1s ease-out 0s 1 normal none running sd-slide-from-left}.sd-animate-slide-from-right{animation:1s ease-out 0s 1 normal none running sd-slide-from-right}.sd-animate-grow100{animation:1s ease-out 0s 1 normal none running sd-grow100}.sd-animate-grow50{animation:1s ease-out 0s 1 normal none running sd-grow50}.sd-animate-grow50-rot20{animation:1s ease-out 0s 1 normal none running sd-grow50-rot20}.sd-badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.sd-badge:empty{display:none}a.sd-badge{text-decoration:none}.sd-btn .sd-badge{position:relative;top:-1px}.sd-btn{background-color:transparent;border:1px solid transparent;border-radius:.25rem;cursor:pointer;display:inline-block;font-weight:400;font-size:1rem;line-height:1.5;padding:.375rem .75rem;text-align:center;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;vertical-align:middle;user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none}.sd-btn:hover{text-decoration:none}@media(prefers-reduced-motion: reduce){.sd-btn{transition:none}}.sd-btn-primary,.sd-btn-outline-primary:hover,.sd-btn-outline-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-primary:hover,.sd-btn-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary-highlight) !important;border-color:var(--sd-color-primary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-primary{color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary,.sd-btn-outline-secondary:hover,.sd-btn-outline-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary:hover,.sd-btn-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary-highlight) !important;border-color:var(--sd-color-secondary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-secondary{color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success,.sd-btn-outline-success:hover,.sd-btn-outline-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success:hover,.sd-btn-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success-highlight) !important;border-color:var(--sd-color-success-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-success{color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info,.sd-btn-outline-info:hover,.sd-btn-outline-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info:hover,.sd-btn-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info-highlight) !important;border-color:var(--sd-color-info-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-info{color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning,.sd-btn-outline-warning:hover,.sd-btn-outline-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning:hover,.sd-btn-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning-highlight) !important;border-color:var(--sd-color-warning-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-warning{color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger,.sd-btn-outline-danger:hover,.sd-btn-outline-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger:hover,.sd-btn-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger-highlight) !important;border-color:var(--sd-color-danger-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-danger{color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light,.sd-btn-outline-light:hover,.sd-btn-outline-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light:hover,.sd-btn-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light-highlight) !important;border-color:var(--sd-color-light-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-light{color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted,.sd-btn-outline-muted:hover,.sd-btn-outline-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted:hover,.sd-btn-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted-highlight) !important;border-color:var(--sd-color-muted-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-muted{color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark,.sd-btn-outline-dark:hover,.sd-btn-outline-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark:hover,.sd-btn-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark-highlight) !important;border-color:var(--sd-color-dark-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-dark{color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black,.sd-btn-outline-black:hover,.sd-btn-outline-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black:hover,.sd-btn-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black-highlight) !important;border-color:var(--sd-color-black-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-black{color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white,.sd-btn-outline-white:hover,.sd-btn-outline-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white:hover,.sd-btn-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white-highlight) !important;border-color:var(--sd-color-white-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-white{color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.sd-hide-link-text{font-size:0}.sd-octicon,.sd-material-icon{display:inline-block;fill:currentColor;vertical-align:middle}.sd-avatar-xs{border-radius:50%;object-fit:cover;object-position:center;width:1rem;height:1rem}.sd-avatar-sm{border-radius:50%;object-fit:cover;object-position:center;width:3rem;height:3rem}.sd-avatar-md{border-radius:50%;object-fit:cover;object-position:center;width:5rem;height:5rem}.sd-avatar-lg{border-radius:50%;object-fit:cover;object-position:center;width:7rem;height:7rem}.sd-avatar-xl{border-radius:50%;object-fit:cover;object-position:center;width:10rem;height:10rem}.sd-avatar-inherit{border-radius:50%;object-fit:cover;object-position:center;width:inherit;height:inherit}.sd-avatar-initial{border-radius:50%;object-fit:cover;object-position:center;width:initial;height:initial}.sd-card{background-clip:border-box;background-color:var(--sd-color-card-background);border:1px solid var(--sd-color-card-border);border-radius:.25rem;color:var(--sd-color-card-text);display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;position:relative;word-wrap:break-word}.sd-card>hr{margin-left:0;margin-right:0}.sd-card-hover:hover{border-color:var(--sd-color-card-border-hover);transform:scale(1.01)}.sd-card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem 1rem}.sd-card-title{margin-bottom:.5rem}.sd-card-subtitle{margin-top:-0.25rem;margin-bottom:0}.sd-card-text:last-child{margin-bottom:0}.sd-card-link:hover{text-decoration:none}.sd-card-link+.card-link{margin-left:1rem}.sd-card-header{padding:.5rem 1rem;margin-bottom:0;background-color:var(--sd-color-card-header);border-bottom:1px solid var(--sd-color-card-border)}.sd-card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.sd-card-footer{padding:.5rem 1rem;background-color:var(--sd-color-card-footer);border-top:1px solid var(--sd-color-card-border)}.sd-card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.sd-card-header-tabs{margin-right:-0.5rem;margin-bottom:-0.5rem;margin-left:-0.5rem;border-bottom:0}.sd-card-header-pills{margin-right:-0.5rem;margin-left:-0.5rem}.sd-card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom,.sd-card-img-top{width:100%}.sd-card-img,.sd-card-img-top{border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom{border-bottom-left-radius:calc(0.25rem - 1px);border-bottom-right-radius:calc(0.25rem - 1px)}.sd-cards-carousel{width:100%;display:flex;flex-wrap:nowrap;-ms-flex-direction:row;flex-direction:row;overflow-x:hidden;scroll-snap-type:x mandatory}.sd-cards-carousel.sd-show-scrollbar{overflow-x:auto}.sd-cards-carousel:hover,.sd-cards-carousel:focus{overflow-x:auto}.sd-cards-carousel>.sd-card{flex-shrink:0;scroll-snap-align:start}.sd-cards-carousel>.sd-card:not(:last-child){margin-right:3px}.sd-card-cols-1>.sd-card{width:90%}.sd-card-cols-2>.sd-card{width:45%}.sd-card-cols-3>.sd-card{width:30%}.sd-card-cols-4>.sd-card{width:22.5%}.sd-card-cols-5>.sd-card{width:18%}.sd-card-cols-6>.sd-card{width:15%}.sd-card-cols-7>.sd-card{width:12.8571428571%}.sd-card-cols-8>.sd-card{width:11.25%}.sd-card-cols-9>.sd-card{width:10%}.sd-card-cols-10>.sd-card{width:9%}.sd-card-cols-11>.sd-card{width:8.1818181818%}.sd-card-cols-12>.sd-card{width:7.5%}.sd-container,.sd-container-fluid,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container-xl{margin-left:auto;margin-right:auto;padding-left:var(--sd-gutter-x, 0.75rem);padding-right:var(--sd-gutter-x, 0.75rem);width:100%}@media(min-width: 576px){.sd-container-sm,.sd-container{max-width:540px}}@media(min-width: 768px){.sd-container-md,.sd-container-sm,.sd-container{max-width:720px}}@media(min-width: 992px){.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:960px}}@media(min-width: 1200px){.sd-container-xl,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:1140px}}.sd-row{--sd-gutter-x: 1.5rem;--sd-gutter-y: 0;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-top:calc(var(--sd-gutter-y) * -1);margin-right:calc(var(--sd-gutter-x) * -0.5);margin-left:calc(var(--sd-gutter-x) * -0.5)}.sd-row>*{box-sizing:border-box;flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--sd-gutter-x) * 0.5);padding-left:calc(var(--sd-gutter-x) * 0.5);margin-top:var(--sd-gutter-y)}.sd-col{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-auto>*{flex:0 0 auto;width:auto}.sd-row-cols-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}@media(min-width: 576px){.sd-col-sm{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-sm-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-sm-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-sm-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-sm-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-sm-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-sm-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-sm-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-sm-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-sm-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-sm-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-sm-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-sm-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-sm-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 768px){.sd-col-md{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-md-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-md-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-md-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-md-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-md-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-md-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-md-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-md-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-md-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-md-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-md-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-md-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-md-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 992px){.sd-col-lg{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-lg-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-lg-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-lg-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-lg-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-lg-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-lg-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-lg-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-lg-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-lg-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-lg-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-lg-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-lg-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-lg-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 1200px){.sd-col-xl{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-xl-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-xl-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-xl-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-xl-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-xl-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-xl-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-xl-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-xl-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-xl-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-xl-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-xl-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-xl-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-xl-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}.sd-col-auto{flex:0 0 auto;-ms-flex:0 0 auto;width:auto}.sd-col-1{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}.sd-col-2{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-col-3{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-col-4{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-col-5{flex:0 0 auto;-ms-flex:0 0 auto;width:41.6666666667%}.sd-col-6{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-col-7{flex:0 0 auto;-ms-flex:0 0 auto;width:58.3333333333%}.sd-col-8{flex:0 0 auto;-ms-flex:0 0 auto;width:66.6666666667%}.sd-col-9{flex:0 0 auto;-ms-flex:0 0 auto;width:75%}.sd-col-10{flex:0 0 auto;-ms-flex:0 0 auto;width:83.3333333333%}.sd-col-11{flex:0 0 auto;-ms-flex:0 0 auto;width:91.6666666667%}.sd-col-12{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-g-0,.sd-gy-0{--sd-gutter-y: 0}.sd-g-0,.sd-gx-0{--sd-gutter-x: 0}.sd-g-1,.sd-gy-1{--sd-gutter-y: 0.25rem}.sd-g-1,.sd-gx-1{--sd-gutter-x: 0.25rem}.sd-g-2,.sd-gy-2{--sd-gutter-y: 0.5rem}.sd-g-2,.sd-gx-2{--sd-gutter-x: 0.5rem}.sd-g-3,.sd-gy-3{--sd-gutter-y: 1rem}.sd-g-3,.sd-gx-3{--sd-gutter-x: 1rem}.sd-g-4,.sd-gy-4{--sd-gutter-y: 1.5rem}.sd-g-4,.sd-gx-4{--sd-gutter-x: 1.5rem}.sd-g-5,.sd-gy-5{--sd-gutter-y: 3rem}.sd-g-5,.sd-gx-5{--sd-gutter-x: 3rem}@media(min-width: 576px){.sd-col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-sm-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-sm-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-sm-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-sm-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-sm-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-sm-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-sm-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-sm-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-sm-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-sm-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-sm-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-sm-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-sm-0,.sd-gy-sm-0{--sd-gutter-y: 0}.sd-g-sm-0,.sd-gx-sm-0{--sd-gutter-x: 0}.sd-g-sm-1,.sd-gy-sm-1{--sd-gutter-y: 0.25rem}.sd-g-sm-1,.sd-gx-sm-1{--sd-gutter-x: 0.25rem}.sd-g-sm-2,.sd-gy-sm-2{--sd-gutter-y: 0.5rem}.sd-g-sm-2,.sd-gx-sm-2{--sd-gutter-x: 0.5rem}.sd-g-sm-3,.sd-gy-sm-3{--sd-gutter-y: 1rem}.sd-g-sm-3,.sd-gx-sm-3{--sd-gutter-x: 1rem}.sd-g-sm-4,.sd-gy-sm-4{--sd-gutter-y: 1.5rem}.sd-g-sm-4,.sd-gx-sm-4{--sd-gutter-x: 1.5rem}.sd-g-sm-5,.sd-gy-sm-5{--sd-gutter-y: 3rem}.sd-g-sm-5,.sd-gx-sm-5{--sd-gutter-x: 3rem}}@media(min-width: 768px){.sd-col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-md-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-md-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-md-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-md-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-md-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-md-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-md-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-md-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-md-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-md-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-md-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-md-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-md-0,.sd-gy-md-0{--sd-gutter-y: 0}.sd-g-md-0,.sd-gx-md-0{--sd-gutter-x: 0}.sd-g-md-1,.sd-gy-md-1{--sd-gutter-y: 0.25rem}.sd-g-md-1,.sd-gx-md-1{--sd-gutter-x: 0.25rem}.sd-g-md-2,.sd-gy-md-2{--sd-gutter-y: 0.5rem}.sd-g-md-2,.sd-gx-md-2{--sd-gutter-x: 0.5rem}.sd-g-md-3,.sd-gy-md-3{--sd-gutter-y: 1rem}.sd-g-md-3,.sd-gx-md-3{--sd-gutter-x: 1rem}.sd-g-md-4,.sd-gy-md-4{--sd-gutter-y: 1.5rem}.sd-g-md-4,.sd-gx-md-4{--sd-gutter-x: 1.5rem}.sd-g-md-5,.sd-gy-md-5{--sd-gutter-y: 3rem}.sd-g-md-5,.sd-gx-md-5{--sd-gutter-x: 3rem}}@media(min-width: 992px){.sd-col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-lg-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-lg-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-lg-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-lg-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-lg-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-lg-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-lg-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-lg-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-lg-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-lg-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-lg-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-lg-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-lg-0,.sd-gy-lg-0{--sd-gutter-y: 0}.sd-g-lg-0,.sd-gx-lg-0{--sd-gutter-x: 0}.sd-g-lg-1,.sd-gy-lg-1{--sd-gutter-y: 0.25rem}.sd-g-lg-1,.sd-gx-lg-1{--sd-gutter-x: 0.25rem}.sd-g-lg-2,.sd-gy-lg-2{--sd-gutter-y: 0.5rem}.sd-g-lg-2,.sd-gx-lg-2{--sd-gutter-x: 0.5rem}.sd-g-lg-3,.sd-gy-lg-3{--sd-gutter-y: 1rem}.sd-g-lg-3,.sd-gx-lg-3{--sd-gutter-x: 1rem}.sd-g-lg-4,.sd-gy-lg-4{--sd-gutter-y: 1.5rem}.sd-g-lg-4,.sd-gx-lg-4{--sd-gutter-x: 1.5rem}.sd-g-lg-5,.sd-gy-lg-5{--sd-gutter-y: 3rem}.sd-g-lg-5,.sd-gx-lg-5{--sd-gutter-x: 3rem}}@media(min-width: 1200px){.sd-col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-xl-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-xl-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-xl-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-xl-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-xl-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-xl-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-xl-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-xl-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-xl-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-xl-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-xl-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-xl-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-xl-0,.sd-gy-xl-0{--sd-gutter-y: 0}.sd-g-xl-0,.sd-gx-xl-0{--sd-gutter-x: 0}.sd-g-xl-1,.sd-gy-xl-1{--sd-gutter-y: 0.25rem}.sd-g-xl-1,.sd-gx-xl-1{--sd-gutter-x: 0.25rem}.sd-g-xl-2,.sd-gy-xl-2{--sd-gutter-y: 0.5rem}.sd-g-xl-2,.sd-gx-xl-2{--sd-gutter-x: 0.5rem}.sd-g-xl-3,.sd-gy-xl-3{--sd-gutter-y: 1rem}.sd-g-xl-3,.sd-gx-xl-3{--sd-gutter-x: 1rem}.sd-g-xl-4,.sd-gy-xl-4{--sd-gutter-y: 1.5rem}.sd-g-xl-4,.sd-gx-xl-4{--sd-gutter-x: 1.5rem}.sd-g-xl-5,.sd-gy-xl-5{--sd-gutter-y: 3rem}.sd-g-xl-5,.sd-gx-xl-5{--sd-gutter-x: 3rem}}.sd-flex-row-reverse{flex-direction:row-reverse !important}details.sd-dropdown{position:relative}details.sd-dropdown .sd-summary-title{font-weight:700;padding-right:3em !important;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none}details.sd-dropdown:hover{cursor:pointer}details.sd-dropdown .sd-summary-content{cursor:default}details.sd-dropdown summary{list-style:none;padding:1em}details.sd-dropdown summary .sd-octicon.no-title{vertical-align:middle}details.sd-dropdown[open] summary .sd-octicon.no-title{visibility:hidden}details.sd-dropdown summary::-webkit-details-marker{display:none}details.sd-dropdown summary:focus{outline:none}details.sd-dropdown .sd-summary-icon{margin-right:.5em}details.sd-dropdown .sd-summary-icon svg{opacity:.8}details.sd-dropdown summary:hover .sd-summary-up svg,details.sd-dropdown summary:hover .sd-summary-down svg{opacity:1;transform:scale(1.1)}details.sd-dropdown .sd-summary-up svg,details.sd-dropdown .sd-summary-down svg{display:block;opacity:.6}details.sd-dropdown .sd-summary-up,details.sd-dropdown .sd-summary-down{pointer-events:none;position:absolute;right:1em;top:1em}details.sd-dropdown[open]>.sd-summary-title .sd-summary-down{visibility:hidden}details.sd-dropdown:not([open])>.sd-summary-title .sd-summary-up{visibility:hidden}details.sd-dropdown:not([open]).sd-card{border:none}details.sd-dropdown:not([open])>.sd-card-header{border:1px solid var(--sd-color-card-border);border-radius:.25rem}details.sd-dropdown.sd-fade-in[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out;animation:sd-fade-in .5s ease-in-out}details.sd-dropdown.sd-fade-in-slide-down[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out}.sd-col>.sd-dropdown{width:100%}.sd-summary-content>.sd-tab-set:first-child{margin-top:0}@keyframes sd-fade-in{0%{opacity:0}100%{opacity:1}}@keyframes sd-slide-down{0%{transform:translate(0, -10px)}100%{transform:translate(0, 0)}}.sd-tab-set{border-radius:.125rem;display:flex;flex-wrap:wrap;margin:1em 0;position:relative}.sd-tab-set>input{opacity:0;position:absolute}.sd-tab-set>input:checked+label{border-color:var(--sd-color-tabs-underline-active);color:var(--sd-color-tabs-label-active)}.sd-tab-set>input:checked+label+.sd-tab-content{display:block}.sd-tab-set>input:not(:checked)+label:hover{color:var(--sd-color-tabs-label-hover);border-color:var(--sd-color-tabs-underline-hover)}.sd-tab-set>input:focus+label{outline-style:auto}.sd-tab-set>input:not(.focus-visible)+label{outline:none;-webkit-tap-highlight-color:transparent}.sd-tab-set>label{border-bottom:.125rem solid transparent;margin-bottom:0;color:var(--sd-color-tabs-label-inactive);border-color:var(--sd-color-tabs-underline-inactive);cursor:pointer;font-size:var(--sd-fontsize-tabs-label);font-weight:700;padding:1em 1.25em .5em;transition:color 250ms;width:auto;z-index:1}html .sd-tab-set>label:hover{color:var(--sd-color-tabs-label-active)}.sd-col>.sd-tab-set{width:100%}.sd-tab-content{box-shadow:0 -0.0625rem var(--sd-color-tabs-overline),0 .0625rem var(--sd-color-tabs-underline);display:none;order:99;padding-bottom:.75rem;padding-top:.75rem;width:100%}.sd-tab-content>:first-child{margin-top:0 !important}.sd-tab-content>:last-child{margin-bottom:0 !important}.sd-tab-content>.sd-tab-set{margin:0}.sd-sphinx-override,.sd-sphinx-override *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.sd-sphinx-override p{margin-top:0}:root{--sd-color-primary: #0071bc;--sd-color-secondary: #6c757d;--sd-color-success: #28a745;--sd-color-info: #17a2b8;--sd-color-warning: #f0b37e;--sd-color-danger: #dc3545;--sd-color-light: #f8f9fa;--sd-color-muted: #6c757d;--sd-color-dark: #212529;--sd-color-black: black;--sd-color-white: white;--sd-color-primary-highlight: #0060a0;--sd-color-secondary-highlight: #5c636a;--sd-color-success-highlight: #228e3b;--sd-color-info-highlight: #148a9c;--sd-color-warning-highlight: #cc986b;--sd-color-danger-highlight: #bb2d3b;--sd-color-light-highlight: #d3d4d5;--sd-color-muted-highlight: #5c636a;--sd-color-dark-highlight: #1c1f23;--sd-color-black-highlight: black;--sd-color-white-highlight: #d9d9d9;--sd-color-primary-text: #fff;--sd-color-secondary-text: #fff;--sd-color-success-text: #fff;--sd-color-info-text: #fff;--sd-color-warning-text: #212529;--sd-color-danger-text: #fff;--sd-color-light-text: #212529;--sd-color-muted-text: #fff;--sd-color-dark-text: #fff;--sd-color-black-text: #fff;--sd-color-white-text: #212529;--sd-color-shadow: rgba(0, 0, 0, 0.15);--sd-color-card-border: rgba(0, 0, 0, 0.125);--sd-color-card-border-hover: hsla(231, 99%, 66%, 1);--sd-color-card-background: transparent;--sd-color-card-text: inherit;--sd-color-card-header: transparent;--sd-color-card-footer: transparent;--sd-color-tabs-label-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-hover: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-inactive: hsl(0, 0%, 66%);--sd-color-tabs-underline-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-underline-hover: rgba(178, 206, 245, 0.62);--sd-color-tabs-underline-inactive: transparent;--sd-color-tabs-overline: rgb(222, 222, 222);--sd-color-tabs-underline: rgb(222, 222, 222);--sd-fontsize-tabs-label: 1rem} diff --git a/main/_static/design-tabs.js b/main/_static/design-tabs.js new file mode 100644 index 0000000..36b38cf --- /dev/null +++ b/main/_static/design-tabs.js @@ -0,0 +1,27 @@ +var sd_labels_by_text = {}; + +function ready() { + const li = document.getElementsByClassName("sd-tab-label"); + for (const label of li) { + syncId = label.getAttribute("data-sync-id"); + if (syncId) { + label.onclick = onLabelClick; + if (!sd_labels_by_text[syncId]) { + sd_labels_by_text[syncId] = []; + } + sd_labels_by_text[syncId].push(label); + } + } +} + +function onLabelClick() { + // Activate other inputs with the same sync id. + syncId = this.getAttribute("data-sync-id"); + for (label of sd_labels_by_text[syncId]) { + if (label === this) continue; + label.previousElementSibling.checked = true; + } + window.localStorage.setItem("sphinx-design-last-tab", syncId); +} + +document.addEventListener("DOMContentLoaded", ready, false); diff --git a/main/_static/doctools.js b/main/_static/doctools.js new file mode 100644 index 0000000..d06a71d --- /dev/null +++ b/main/_static/doctools.js @@ -0,0 +1,156 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Base JavaScript utilities for all Sphinx HTML documentation. + * + * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/main/_static/documentation_options.js b/main/_static/documentation_options.js new file mode 100644 index 0000000..dab586c --- /dev/null +++ b/main/_static/documentation_options.js @@ -0,0 +1,13 @@ +const DOCUMENTATION_OPTIONS = { + VERSION: '', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/main/_static/file.png b/main/_static/file.png new file mode 100644 index 0000000..a858a41 Binary files /dev/null and b/main/_static/file.png differ diff --git a/main/_static/font_button.js b/main/_static/font_button.js new file mode 100644 index 0000000..0dbac5d --- /dev/null +++ b/main/_static/font_button.js @@ -0,0 +1,84 @@ +// Accessibility changes (these are a bit hacky / repetitive while I debug javascript) + + +// Persistent variable - defaults to pretty font + +if (localStorage.getItem('legibility') != 'legible') { + localStorage.setItem('legibility', 'pretty'); +} + +// Persistent variable - scale defaults to 1.0 + +if (localStorage.getItem('fontScale') == null) { + localStorage.setItem('fontScale', 1.0); +} + +// Action this on page load, will use persistent values + +// Font scale factor + +fontScalingFactor = localStorage.getItem('fontScale') +document.documentElement.style.setProperty('--pst-font-scale-factor', fontScalingFactor); + +// Font families + +savedStyle = localStorage.getItem('legibility') +if (savedStyle == 'legible') + legibleFontSetter('legible') +else + legibleFontSetter('pretty') + + +// Change font size + +function fontScaler(scale){ + if(scale == 0.0){ + if (localStorage.getItem('legibility') == 'legible') + fontScalingFactor = 1.25; + else + fontScalingFactor = 1.0; + } + else { + fontScalingFactor = localStorage.getItem('fontScale') + fontScalingFactor *= scale + } + localStorage.setItem('fontScale', fontScalingFactor); + document.documentElement.style.setProperty('--pst-font-scale-factor', fontScalingFactor); +} + +// Change the font family + +function legibleFontSetter(fontType){ + if (fontType == 'legible'){ + legibleFontFamily = getComputedStyle(document.documentElement).getPropertyValue('--pst-font-family-legible'); + legibleFontFamilyH = getComputedStyle(document.documentElement).getPropertyValue('--pst-font-family-legible-headers'); + document.documentElement.style.setProperty('--pst-font-family-base', legibleFontFamily); + document.documentElement.style.setProperty('--pst-font-family-heading', legibleFontFamilyH); + } + else { + prettyFontFamily = getComputedStyle(document.documentElement).getPropertyValue('--pst-font-family-pretty'); + prettyFontFamilyH = getComputedStyle(document.documentElement).getPropertyValue('--pst-font-family-pretty-headers'); + document.documentElement.style.setProperty('--pst-font-family-base', prettyFontFamily); + document.documentElement.style.setProperty('--pst-font-family-heading', prettyFontFamilyH); + } + +} + +// Toggle between legible and pretty fonts + +function legibleFontSwitcher() { + savedStyle = localStorage.getItem('legibility') + if (savedStyle == 'pretty') { + thisStyle = 'legible' + fontScaler(1.25); + legibleFontSetter('legible') + } + else { + thisStyle = 'pretty'; + fontScaler(0.8); + legibleFontSetter('pretty') + } + localStorage.setItem('legibility', thisStyle); +} + + diff --git a/main/_static/images/logo_binder.svg b/main/_static/images/logo_binder.svg new file mode 100644 index 0000000..45fecf7 --- /dev/null +++ b/main/_static/images/logo_binder.svg @@ -0,0 +1,19 @@ + + + + +logo + + + + + + + + diff --git a/main/_static/images/logo_colab.png b/main/_static/images/logo_colab.png new file mode 100644 index 0000000..b7560ec Binary files /dev/null and b/main/_static/images/logo_colab.png differ diff --git a/main/_static/images/logo_deepnote.svg b/main/_static/images/logo_deepnote.svg new file mode 100644 index 0000000..fa77ebf --- /dev/null +++ b/main/_static/images/logo_deepnote.svg @@ -0,0 +1 @@ + diff --git a/main/_static/images/logo_jupyterhub.svg b/main/_static/images/logo_jupyterhub.svg new file mode 100644 index 0000000..60cfe9f --- /dev/null +++ b/main/_static/images/logo_jupyterhub.svg @@ -0,0 +1 @@ +logo_jupyterhubHub diff --git a/main/_static/language_data.js b/main/_static/language_data.js new file mode 100644 index 0000000..250f566 --- /dev/null +++ b/main/_static/language_data.js @@ -0,0 +1,199 @@ +/* + * language_data.js + * ~~~~~~~~~~~~~~~~ + * + * This script contains the language-specific data used by searchtools.js, + * namely the list of stopwords, stemmer, scorer and splitter. + * + * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; + + +/* Non-minified version is copied as a separate JS file, is available */ + +/** + * Porter Stemmer + */ +var Stemmer = function() { + + var step2list = { + ational: 'ate', + tional: 'tion', + enci: 'ence', + anci: 'ance', + izer: 'ize', + bli: 'ble', + alli: 'al', + entli: 'ent', + eli: 'e', + ousli: 'ous', + ization: 'ize', + ation: 'ate', + ator: 'ate', + alism: 'al', + iveness: 'ive', + fulness: 'ful', + ousness: 'ous', + aliti: 'al', + iviti: 'ive', + biliti: 'ble', + logi: 'log' + }; + + var step3list = { + icate: 'ic', + ative: '', + alize: 'al', + iciti: 'ic', + ical: 'ic', + ful: '', + ness: '' + }; + + var c = "[^aeiou]"; // consonant + var v = "[aeiouy]"; // vowel + var C = c + "[^aeiouy]*"; // consonant sequence + var V = v + "[aeiou]*"; // vowel sequence + + var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + diff --git a/main/_static/locales/ar/LC_MESSAGES/booktheme.mo b/main/_static/locales/ar/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..15541a6 Binary files /dev/null and b/main/_static/locales/ar/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/ar/LC_MESSAGES/booktheme.po b/main/_static/locales/ar/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..edae2ec --- /dev/null +++ b/main/_static/locales/ar/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ar\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "موضوع بواسطة" + +msgid "Open an issue" +msgstr "افتح قضية" + +msgid "Contents" +msgstr "محتويات" + +msgid "Download notebook file" +msgstr "تنزيل ملف دفتر الملاحظات" + +msgid "Sphinx Book Theme" +msgstr "موضوع كتاب أبو الهول" + +msgid "Fullscreen mode" +msgstr "وضع ملء الشاشة" + +msgid "Edit this page" +msgstr "قم بتحرير هذه الصفحة" + +msgid "By" +msgstr "بواسطة" + +msgid "Copyright" +msgstr "حقوق النشر" + +msgid "Source repository" +msgstr "مستودع المصدر" + +msgid "previous page" +msgstr "الصفحة السابقة" + +msgid "next page" +msgstr "الصفحة التالية" + +msgid "Toggle navigation" +msgstr "تبديل التنقل" + +msgid "repository" +msgstr "مخزن" + +msgid "suggest edit" +msgstr "أقترح تحرير" + +msgid "open issue" +msgstr "قضية مفتوحة" + +msgid "Launch" +msgstr "إطلاق" + +msgid "Print to PDF" +msgstr "طباعة إلى PDF" + +msgid "By the" +msgstr "بواسطة" + +msgid "Last updated on" +msgstr "آخر تحديث في" + +msgid "Download source file" +msgstr "تنزيل ملف المصدر" + +msgid "Download this page" +msgstr "قم بتنزيل هذه الصفحة" diff --git a/main/_static/locales/bg/LC_MESSAGES/booktheme.mo b/main/_static/locales/bg/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..da95120 Binary files /dev/null and b/main/_static/locales/bg/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/bg/LC_MESSAGES/booktheme.po b/main/_static/locales/bg/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..1f363b9 --- /dev/null +++ b/main/_static/locales/bg/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: bg\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Тема от" + +msgid "Open an issue" +msgstr "Отворете проблем" + +msgid "Contents" +msgstr "Съдържание" + +msgid "Download notebook file" +msgstr "Изтеглете файла на бележника" + +msgid "Sphinx Book Theme" +msgstr "Тема на книгата Sphinx" + +msgid "Fullscreen mode" +msgstr "Режим на цял екран" + +msgid "Edit this page" +msgstr "Редактирайте тази страница" + +msgid "By" +msgstr "От" + +msgid "Copyright" +msgstr "Авторско право" + +msgid "Source repository" +msgstr "Хранилище на източника" + +msgid "previous page" +msgstr "предишна страница" + +msgid "next page" +msgstr "Следваща страница" + +msgid "Toggle navigation" +msgstr "Превключване на навигацията" + +msgid "repository" +msgstr "хранилище" + +msgid "suggest edit" +msgstr "предложи редактиране" + +msgid "open issue" +msgstr "отворен брой" + +msgid "Launch" +msgstr "Стартиране" + +msgid "Print to PDF" +msgstr "Печат в PDF" + +msgid "By the" +msgstr "По" + +msgid "Last updated on" +msgstr "Последна актуализация на" + +msgid "Download source file" +msgstr "Изтеглете изходния файл" + +msgid "Download this page" +msgstr "Изтеглете тази страница" diff --git a/main/_static/locales/bn/LC_MESSAGES/booktheme.mo b/main/_static/locales/bn/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..6b96639 Binary files /dev/null and b/main/_static/locales/bn/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/bn/LC_MESSAGES/booktheme.po b/main/_static/locales/bn/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..fa54372 --- /dev/null +++ b/main/_static/locales/bn/LC_MESSAGES/booktheme.po @@ -0,0 +1,63 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: bn\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "থিম দ্বারা" + +msgid "Open an issue" +msgstr "একটি সমস্যা খুলুন" + +msgid "Download notebook file" +msgstr "নোটবুক ফাইল ডাউনলোড করুন" + +msgid "Sphinx Book Theme" +msgstr "স্পিনিক্স বুক থিম" + +msgid "Edit this page" +msgstr "এই পৃষ্ঠাটি সম্পাদনা করুন" + +msgid "By" +msgstr "দ্বারা" + +msgid "Copyright" +msgstr "কপিরাইট" + +msgid "Source repository" +msgstr "উত্স সংগ্রহস্থল" + +msgid "previous page" +msgstr "আগের পৃষ্ঠা" + +msgid "next page" +msgstr "পরবর্তী পৃষ্ঠা" + +msgid "Toggle navigation" +msgstr "নেভিগেশন টগল করুন" + +msgid "open issue" +msgstr "খোলা সমস্যা" + +msgid "Launch" +msgstr "শুরু করা" + +msgid "Print to PDF" +msgstr "পিডিএফ প্রিন্ট করুন" + +msgid "By the" +msgstr "দ্বারা" + +msgid "Last updated on" +msgstr "সর্বশেষ আপডেট" + +msgid "Download source file" +msgstr "উত্স ফাইল ডাউনলোড করুন" + +msgid "Download this page" +msgstr "এই পৃষ্ঠাটি ডাউনলোড করুন" diff --git a/main/_static/locales/ca/LC_MESSAGES/booktheme.mo b/main/_static/locales/ca/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..a4dd30e Binary files /dev/null and b/main/_static/locales/ca/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/ca/LC_MESSAGES/booktheme.po b/main/_static/locales/ca/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..22f1569 --- /dev/null +++ b/main/_static/locales/ca/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ca\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema del" + +msgid "Open an issue" +msgstr "Obriu un número" + +msgid "Download notebook file" +msgstr "Descarregar fitxer de quadern" + +msgid "Sphinx Book Theme" +msgstr "Tema del llibre Esfinx" + +msgid "Edit this page" +msgstr "Editeu aquesta pàgina" + +msgid "By" +msgstr "Per" + +msgid "Copyright" +msgstr "Copyright" + +msgid "Source repository" +msgstr "Dipòsit de fonts" + +msgid "previous page" +msgstr "Pàgina anterior" + +msgid "next page" +msgstr "pàgina següent" + +msgid "Toggle navigation" +msgstr "Commuta la navegació" + +msgid "suggest edit" +msgstr "suggerir edició" + +msgid "open issue" +msgstr "número obert" + +msgid "Launch" +msgstr "Llançament" + +msgid "Print to PDF" +msgstr "Imprimeix a PDF" + +msgid "By the" +msgstr "Per la" + +msgid "Last updated on" +msgstr "Darrera actualització el" + +msgid "Download source file" +msgstr "Baixeu el fitxer font" + +msgid "Download this page" +msgstr "Descarregueu aquesta pàgina" diff --git a/main/_static/locales/cs/LC_MESSAGES/booktheme.mo b/main/_static/locales/cs/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..c39e01a Binary files /dev/null and b/main/_static/locales/cs/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/cs/LC_MESSAGES/booktheme.po b/main/_static/locales/cs/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..afecd9e --- /dev/null +++ b/main/_static/locales/cs/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: cs\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Téma od" + +msgid "Open an issue" +msgstr "Otevřete problém" + +msgid "Contents" +msgstr "Obsah" + +msgid "Download notebook file" +msgstr "Stáhnout soubor poznámkového bloku" + +msgid "Sphinx Book Theme" +msgstr "Téma knihy Sfinga" + +msgid "Fullscreen mode" +msgstr "Režim celé obrazovky" + +msgid "Edit this page" +msgstr "Upravit tuto stránku" + +msgid "By" +msgstr "Podle" + +msgid "Copyright" +msgstr "autorská práva" + +msgid "Source repository" +msgstr "Zdrojové úložiště" + +msgid "previous page" +msgstr "předchozí stránka" + +msgid "next page" +msgstr "další strana" + +msgid "Toggle navigation" +msgstr "Přepnout navigaci" + +msgid "repository" +msgstr "úložiště" + +msgid "suggest edit" +msgstr "navrhnout úpravy" + +msgid "open issue" +msgstr "otevřené číslo" + +msgid "Launch" +msgstr "Zahájení" + +msgid "Print to PDF" +msgstr "Tisk do PDF" + +msgid "By the" +msgstr "Podle" + +msgid "Last updated on" +msgstr "Naposledy aktualizováno" + +msgid "Download source file" +msgstr "Stáhněte si zdrojový soubor" + +msgid "Download this page" +msgstr "Stáhněte si tuto stránku" diff --git a/main/_static/locales/da/LC_MESSAGES/booktheme.mo b/main/_static/locales/da/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..f43157d Binary files /dev/null and b/main/_static/locales/da/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/da/LC_MESSAGES/booktheme.po b/main/_static/locales/da/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..649c78a --- /dev/null +++ b/main/_static/locales/da/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: da\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema af" + +msgid "Open an issue" +msgstr "Åbn et problem" + +msgid "Contents" +msgstr "Indhold" + +msgid "Download notebook file" +msgstr "Download notesbog-fil" + +msgid "Sphinx Book Theme" +msgstr "Sphinx bogtema" + +msgid "Fullscreen mode" +msgstr "Fuldskærmstilstand" + +msgid "Edit this page" +msgstr "Rediger denne side" + +msgid "By" +msgstr "Ved" + +msgid "Copyright" +msgstr "ophavsret" + +msgid "Source repository" +msgstr "Kildelager" + +msgid "previous page" +msgstr "forrige side" + +msgid "next page" +msgstr "Næste side" + +msgid "Toggle navigation" +msgstr "Skift navigation" + +msgid "repository" +msgstr "lager" + +msgid "suggest edit" +msgstr "foreslå redigering" + +msgid "open issue" +msgstr "åbent nummer" + +msgid "Launch" +msgstr "Start" + +msgid "Print to PDF" +msgstr "Udskriv til PDF" + +msgid "By the" +msgstr "Ved" + +msgid "Last updated on" +msgstr "Sidst opdateret den" + +msgid "Download source file" +msgstr "Download kildefil" + +msgid "Download this page" +msgstr "Download denne side" diff --git a/main/_static/locales/de/LC_MESSAGES/booktheme.mo b/main/_static/locales/de/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..648b565 Binary files /dev/null and b/main/_static/locales/de/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/de/LC_MESSAGES/booktheme.po b/main/_static/locales/de/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..f51d2ec --- /dev/null +++ b/main/_static/locales/de/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: de\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Thema von der" + +msgid "Open an issue" +msgstr "Öffnen Sie ein Problem" + +msgid "Contents" +msgstr "Inhalt" + +msgid "Download notebook file" +msgstr "Notebook-Datei herunterladen" + +msgid "Sphinx Book Theme" +msgstr "Sphinx-Buch-Thema" + +msgid "Fullscreen mode" +msgstr "Vollbildmodus" + +msgid "Edit this page" +msgstr "Bearbeite diese Seite" + +msgid "By" +msgstr "Durch" + +msgid "Copyright" +msgstr "Urheberrechte ©" + +msgid "Source repository" +msgstr "Quell-Repository" + +msgid "previous page" +msgstr "vorherige Seite" + +msgid "next page" +msgstr "Nächste Seite" + +msgid "Toggle navigation" +msgstr "Navigation umschalten" + +msgid "repository" +msgstr "Repository" + +msgid "suggest edit" +msgstr "vorschlagen zu bearbeiten" + +msgid "open issue" +msgstr "offenes Thema" + +msgid "Launch" +msgstr "Starten" + +msgid "Print to PDF" +msgstr "In PDF drucken" + +msgid "By the" +msgstr "Bis zum" + +msgid "Last updated on" +msgstr "Zuletzt aktualisiert am" + +msgid "Download source file" +msgstr "Quelldatei herunterladen" + +msgid "Download this page" +msgstr "Laden Sie diese Seite herunter" diff --git a/main/_static/locales/el/LC_MESSAGES/booktheme.mo b/main/_static/locales/el/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..fca6e93 Binary files /dev/null and b/main/_static/locales/el/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/el/LC_MESSAGES/booktheme.po b/main/_static/locales/el/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..8bec790 --- /dev/null +++ b/main/_static/locales/el/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: el\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Θέμα από το" + +msgid "Open an issue" +msgstr "Ανοίξτε ένα ζήτημα" + +msgid "Contents" +msgstr "Περιεχόμενα" + +msgid "Download notebook file" +msgstr "Λήψη αρχείου σημειωματάριου" + +msgid "Sphinx Book Theme" +msgstr "Θέμα βιβλίου Sphinx" + +msgid "Fullscreen mode" +msgstr "ΛΕΙΤΟΥΡΓΙΑ ΠΛΗΡΟΥΣ ΟΘΟΝΗΣ" + +msgid "Edit this page" +msgstr "Επεξεργαστείτε αυτήν τη σελίδα" + +msgid "By" +msgstr "Με" + +msgid "Copyright" +msgstr "Πνευματική ιδιοκτησία" + +msgid "Source repository" +msgstr "Αποθήκη πηγής" + +msgid "previous page" +msgstr "προηγούμενη σελίδα" + +msgid "next page" +msgstr "επόμενη σελίδα" + +msgid "Toggle navigation" +msgstr "Εναλλαγή πλοήγησης" + +msgid "repository" +msgstr "αποθήκη" + +msgid "suggest edit" +msgstr "προτείνω επεξεργασία" + +msgid "open issue" +msgstr "ανοιχτό ζήτημα" + +msgid "Launch" +msgstr "Εκτόξευση" + +msgid "Print to PDF" +msgstr "Εκτύπωση σε PDF" + +msgid "By the" +msgstr "Από το" + +msgid "Last updated on" +msgstr "Τελευταία ενημέρωση στις" + +msgid "Download source file" +msgstr "Λήψη αρχείου προέλευσης" + +msgid "Download this page" +msgstr "Λήψη αυτής της σελίδας" diff --git a/main/_static/locales/eo/LC_MESSAGES/booktheme.mo b/main/_static/locales/eo/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..d1072bb Binary files /dev/null and b/main/_static/locales/eo/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/eo/LC_MESSAGES/booktheme.po b/main/_static/locales/eo/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..d72a048 --- /dev/null +++ b/main/_static/locales/eo/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: eo\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Temo de la" + +msgid "Open an issue" +msgstr "Malfermu numeron" + +msgid "Contents" +msgstr "Enhavo" + +msgid "Download notebook file" +msgstr "Elŝutu kajeran dosieron" + +msgid "Sphinx Book Theme" +msgstr "Sfinksa Libro-Temo" + +msgid "Fullscreen mode" +msgstr "Plenekrana reĝimo" + +msgid "Edit this page" +msgstr "Redaktu ĉi tiun paĝon" + +msgid "By" +msgstr "De" + +msgid "Copyright" +msgstr "Kopirajto" + +msgid "Source repository" +msgstr "Fonto-deponejo" + +msgid "previous page" +msgstr "antaŭa paĝo" + +msgid "next page" +msgstr "sekva paĝo" + +msgid "Toggle navigation" +msgstr "Ŝalti navigadon" + +msgid "repository" +msgstr "deponejo" + +msgid "suggest edit" +msgstr "sugesti redaktadon" + +msgid "open issue" +msgstr "malferma numero" + +msgid "Launch" +msgstr "Lanĉo" + +msgid "Print to PDF" +msgstr "Presi al PDF" + +msgid "By the" +msgstr "Per la" + +msgid "Last updated on" +msgstr "Laste ĝisdatigita la" + +msgid "Download source file" +msgstr "Elŝutu fontodosieron" + +msgid "Download this page" +msgstr "Elŝutu ĉi tiun paĝon" diff --git a/main/_static/locales/es/LC_MESSAGES/booktheme.mo b/main/_static/locales/es/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..ba2ee4d Binary files /dev/null and b/main/_static/locales/es/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/es/LC_MESSAGES/booktheme.po b/main/_static/locales/es/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..611834b --- /dev/null +++ b/main/_static/locales/es/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: es\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema por el" + +msgid "Open an issue" +msgstr "Abrir un problema" + +msgid "Contents" +msgstr "Contenido" + +msgid "Download notebook file" +msgstr "Descargar archivo de cuaderno" + +msgid "Sphinx Book Theme" +msgstr "Tema del libro de la esfinge" + +msgid "Fullscreen mode" +msgstr "Modo de pantalla completa" + +msgid "Edit this page" +msgstr "Edita esta página" + +msgid "By" +msgstr "Por" + +msgid "Copyright" +msgstr "Derechos de autor" + +msgid "Source repository" +msgstr "Repositorio de origen" + +msgid "previous page" +msgstr "pagina anterior" + +msgid "next page" +msgstr "siguiente página" + +msgid "Toggle navigation" +msgstr "Navegación de palanca" + +msgid "repository" +msgstr "repositorio" + +msgid "suggest edit" +msgstr "sugerir editar" + +msgid "open issue" +msgstr "Tema abierto" + +msgid "Launch" +msgstr "Lanzamiento" + +msgid "Print to PDF" +msgstr "Imprimir en PDF" + +msgid "By the" +msgstr "Por el" + +msgid "Last updated on" +msgstr "Ultima actualización en" + +msgid "Download source file" +msgstr "Descargar archivo fuente" + +msgid "Download this page" +msgstr "Descarga esta pagina" diff --git a/main/_static/locales/et/LC_MESSAGES/booktheme.mo b/main/_static/locales/et/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..983b823 Binary files /dev/null and b/main/_static/locales/et/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/et/LC_MESSAGES/booktheme.po b/main/_static/locales/et/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..345088f --- /dev/null +++ b/main/_static/locales/et/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: et\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Teema" + +msgid "Open an issue" +msgstr "Avage probleem" + +msgid "Contents" +msgstr "Sisu" + +msgid "Download notebook file" +msgstr "Laadige sülearvuti fail alla" + +msgid "Sphinx Book Theme" +msgstr "Sfinksiraamatu teema" + +msgid "Fullscreen mode" +msgstr "Täisekraanirežiim" + +msgid "Edit this page" +msgstr "Muutke seda lehte" + +msgid "By" +msgstr "Kõrval" + +msgid "Copyright" +msgstr "Autoriõigus" + +msgid "Source repository" +msgstr "Allikahoidla" + +msgid "previous page" +msgstr "eelmine leht" + +msgid "next page" +msgstr "järgmine leht" + +msgid "Toggle navigation" +msgstr "Lülita navigeerimine sisse" + +msgid "repository" +msgstr "hoidla" + +msgid "suggest edit" +msgstr "soovita muuta" + +msgid "open issue" +msgstr "avatud küsimus" + +msgid "Launch" +msgstr "Käivitage" + +msgid "Print to PDF" +msgstr "Prindi PDF-i" + +msgid "By the" +msgstr "Autor" + +msgid "Last updated on" +msgstr "Viimati uuendatud" + +msgid "Download source file" +msgstr "Laadige alla lähtefail" + +msgid "Download this page" +msgstr "Laadige see leht alla" diff --git a/main/_static/locales/fi/LC_MESSAGES/booktheme.mo b/main/_static/locales/fi/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..d8ac054 Binary files /dev/null and b/main/_static/locales/fi/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/fi/LC_MESSAGES/booktheme.po b/main/_static/locales/fi/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..d97a08d --- /dev/null +++ b/main/_static/locales/fi/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: fi\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Teeman tekijä" + +msgid "Open an issue" +msgstr "Avaa ongelma" + +msgid "Contents" +msgstr "Sisällys" + +msgid "Download notebook file" +msgstr "Lataa muistikirjatiedosto" + +msgid "Sphinx Book Theme" +msgstr "Sphinx-kirjan teema" + +msgid "Fullscreen mode" +msgstr "Koko näytön tila" + +msgid "Edit this page" +msgstr "Muokkaa tätä sivua" + +msgid "By" +msgstr "Tekijä" + +msgid "Copyright" +msgstr "Tekijänoikeus" + +msgid "Source repository" +msgstr "Lähteen arkisto" + +msgid "previous page" +msgstr "Edellinen sivu" + +msgid "next page" +msgstr "seuraava sivu" + +msgid "Toggle navigation" +msgstr "Vaihda navigointia" + +msgid "repository" +msgstr "arkisto" + +msgid "suggest edit" +msgstr "ehdottaa muokkausta" + +msgid "open issue" +msgstr "avoin ongelma" + +msgid "Launch" +msgstr "Tuoda markkinoille" + +msgid "Print to PDF" +msgstr "Tulosta PDF-tiedostoon" + +msgid "By the" +msgstr "Mukaan" + +msgid "Last updated on" +msgstr "Viimeksi päivitetty" + +msgid "Download source file" +msgstr "Lataa lähdetiedosto" + +msgid "Download this page" +msgstr "Lataa tämä sivu" diff --git a/main/_static/locales/fr/LC_MESSAGES/booktheme.mo b/main/_static/locales/fr/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..f663d39 Binary files /dev/null and b/main/_static/locales/fr/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/fr/LC_MESSAGES/booktheme.po b/main/_static/locales/fr/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..88f3517 --- /dev/null +++ b/main/_static/locales/fr/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: fr\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Thème par le" + +msgid "Open an issue" +msgstr "Ouvrez un problème" + +msgid "Contents" +msgstr "Contenu" + +msgid "Download notebook file" +msgstr "Télécharger le fichier notebook" + +msgid "Sphinx Book Theme" +msgstr "Thème du livre Sphinx" + +msgid "Fullscreen mode" +msgstr "Mode plein écran" + +msgid "Edit this page" +msgstr "Modifier cette page" + +msgid "By" +msgstr "Par" + +msgid "Copyright" +msgstr "droits d'auteur" + +msgid "Source repository" +msgstr "Dépôt source" + +msgid "previous page" +msgstr "page précédente" + +msgid "next page" +msgstr "page suivante" + +msgid "Toggle navigation" +msgstr "Basculer la navigation" + +msgid "repository" +msgstr "dépôt" + +msgid "suggest edit" +msgstr "suggestion de modification" + +msgid "open issue" +msgstr "signaler un problème" + +msgid "Launch" +msgstr "lancement" + +msgid "Print to PDF" +msgstr "Imprimer au format PDF" + +msgid "By the" +msgstr "Par le" + +msgid "Last updated on" +msgstr "Dernière mise à jour le" + +msgid "Download source file" +msgstr "Télécharger le fichier source" + +msgid "Download this page" +msgstr "Téléchargez cette page" diff --git a/main/_static/locales/hr/LC_MESSAGES/booktheme.mo b/main/_static/locales/hr/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..eca4a1a Binary files /dev/null and b/main/_static/locales/hr/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/hr/LC_MESSAGES/booktheme.po b/main/_static/locales/hr/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..fb9440a --- /dev/null +++ b/main/_static/locales/hr/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: hr\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema autora" + +msgid "Open an issue" +msgstr "Otvorite izdanje" + +msgid "Contents" +msgstr "Sadržaj" + +msgid "Download notebook file" +msgstr "Preuzmi datoteku bilježnice" + +msgid "Sphinx Book Theme" +msgstr "Tema knjige Sphinx" + +msgid "Fullscreen mode" +msgstr "Način preko cijelog zaslona" + +msgid "Edit this page" +msgstr "Uredite ovu stranicu" + +msgid "By" +msgstr "Po" + +msgid "Copyright" +msgstr "Autorska prava" + +msgid "Source repository" +msgstr "Izvorno spremište" + +msgid "previous page" +msgstr "Prethodna stranica" + +msgid "next page" +msgstr "sljedeća stranica" + +msgid "Toggle navigation" +msgstr "Uključi / isključi navigaciju" + +msgid "repository" +msgstr "spremište" + +msgid "suggest edit" +msgstr "predloži uređivanje" + +msgid "open issue" +msgstr "otvoreno izdanje" + +msgid "Launch" +msgstr "Pokrenite" + +msgid "Print to PDF" +msgstr "Ispis u PDF" + +msgid "By the" +msgstr "Od strane" + +msgid "Last updated on" +msgstr "Posljednje ažuriranje:" + +msgid "Download source file" +msgstr "Preuzmi izvornu datoteku" + +msgid "Download this page" +msgstr "Preuzmite ovu stranicu" diff --git a/main/_static/locales/id/LC_MESSAGES/booktheme.mo b/main/_static/locales/id/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..d07a06a Binary files /dev/null and b/main/_static/locales/id/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/id/LC_MESSAGES/booktheme.po b/main/_static/locales/id/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..9ffb56f --- /dev/null +++ b/main/_static/locales/id/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: id\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema oleh" + +msgid "Open an issue" +msgstr "Buka masalah" + +msgid "Contents" +msgstr "Isi" + +msgid "Download notebook file" +msgstr "Unduh file notebook" + +msgid "Sphinx Book Theme" +msgstr "Tema Buku Sphinx" + +msgid "Fullscreen mode" +msgstr "Mode layar penuh" + +msgid "Edit this page" +msgstr "Edit halaman ini" + +msgid "By" +msgstr "Oleh" + +msgid "Copyright" +msgstr "hak cipta" + +msgid "Source repository" +msgstr "Repositori sumber" + +msgid "previous page" +msgstr "halaman sebelumnya" + +msgid "next page" +msgstr "halaman selanjutnya" + +msgid "Toggle navigation" +msgstr "Alihkan navigasi" + +msgid "repository" +msgstr "gudang" + +msgid "suggest edit" +msgstr "menyarankan edit" + +msgid "open issue" +msgstr "masalah terbuka" + +msgid "Launch" +msgstr "Meluncurkan" + +msgid "Print to PDF" +msgstr "Cetak ke PDF" + +msgid "By the" +msgstr "Oleh" + +msgid "Last updated on" +msgstr "Terakhir diperbarui saat" + +msgid "Download source file" +msgstr "Unduh file sumber" + +msgid "Download this page" +msgstr "Unduh halaman ini" diff --git a/main/_static/locales/it/LC_MESSAGES/booktheme.mo b/main/_static/locales/it/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..53ba476 Binary files /dev/null and b/main/_static/locales/it/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/it/LC_MESSAGES/booktheme.po b/main/_static/locales/it/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..04308dd --- /dev/null +++ b/main/_static/locales/it/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: it\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema di" + +msgid "Open an issue" +msgstr "Apri un problema" + +msgid "Contents" +msgstr "Contenuti" + +msgid "Download notebook file" +msgstr "Scarica il file del taccuino" + +msgid "Sphinx Book Theme" +msgstr "Tema del libro della Sfinge" + +msgid "Fullscreen mode" +msgstr "Modalità schermo intero" + +msgid "Edit this page" +msgstr "Modifica questa pagina" + +msgid "By" +msgstr "Di" + +msgid "Copyright" +msgstr "Diritto d'autore" + +msgid "Source repository" +msgstr "Repository di origine" + +msgid "previous page" +msgstr "pagina precedente" + +msgid "next page" +msgstr "pagina successiva" + +msgid "Toggle navigation" +msgstr "Attiva / disattiva la navigazione" + +msgid "repository" +msgstr "repository" + +msgid "suggest edit" +msgstr "suggerisci modifica" + +msgid "open issue" +msgstr "questione aperta" + +msgid "Launch" +msgstr "Lanciare" + +msgid "Print to PDF" +msgstr "Stampa in PDF" + +msgid "By the" +msgstr "Dal" + +msgid "Last updated on" +msgstr "Ultimo aggiornamento il" + +msgid "Download source file" +msgstr "Scarica il file sorgente" + +msgid "Download this page" +msgstr "Scarica questa pagina" diff --git a/main/_static/locales/iw/LC_MESSAGES/booktheme.mo b/main/_static/locales/iw/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..a45c657 Binary files /dev/null and b/main/_static/locales/iw/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/iw/LC_MESSAGES/booktheme.po b/main/_static/locales/iw/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..4ea190d --- /dev/null +++ b/main/_static/locales/iw/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: iw\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "נושא מאת" + +msgid "Open an issue" +msgstr "פתח גיליון" + +msgid "Contents" +msgstr "תוכן" + +msgid "Download notebook file" +msgstr "הורד קובץ מחברת" + +msgid "Sphinx Book Theme" +msgstr "נושא ספר ספינקס" + +msgid "Fullscreen mode" +msgstr "מצב מסך מלא" + +msgid "Edit this page" +msgstr "ערוך דף זה" + +msgid "By" +msgstr "על ידי" + +msgid "Copyright" +msgstr "זכויות יוצרים" + +msgid "Source repository" +msgstr "מאגר המקורות" + +msgid "previous page" +msgstr "עמוד קודם" + +msgid "next page" +msgstr "עמוד הבא" + +msgid "Toggle navigation" +msgstr "החלף ניווט" + +msgid "repository" +msgstr "מאגר" + +msgid "suggest edit" +msgstr "מציע לערוך" + +msgid "open issue" +msgstr "בעיה פתוחה" + +msgid "Launch" +msgstr "לְהַשִׁיק" + +msgid "Print to PDF" +msgstr "הדפס לקובץ PDF" + +msgid "By the" +msgstr "דרך" + +msgid "Last updated on" +msgstr "עודכן לאחרונה ב" + +msgid "Download source file" +msgstr "הורד את קובץ המקור" + +msgid "Download this page" +msgstr "הורד דף זה" diff --git a/main/_static/locales/ja/LC_MESSAGES/booktheme.mo b/main/_static/locales/ja/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..1cefd29 Binary files /dev/null and b/main/_static/locales/ja/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/ja/LC_MESSAGES/booktheme.po b/main/_static/locales/ja/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..77d5a09 --- /dev/null +++ b/main/_static/locales/ja/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ja\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "のテーマ" + +msgid "Open an issue" +msgstr "問題を報告" + +msgid "Contents" +msgstr "目次" + +msgid "Download notebook file" +msgstr "ノートブックファイルをダウンロード" + +msgid "Sphinx Book Theme" +msgstr "スフィンクスの本のテーマ" + +msgid "Fullscreen mode" +msgstr "全画面モード" + +msgid "Edit this page" +msgstr "このページを編集" + +msgid "By" +msgstr "著者" + +msgid "Copyright" +msgstr "Copyright" + +msgid "Source repository" +msgstr "ソースリポジトリ" + +msgid "previous page" +msgstr "前のページ" + +msgid "next page" +msgstr "次のページ" + +msgid "Toggle navigation" +msgstr "ナビゲーションを切り替え" + +msgid "repository" +msgstr "リポジトリ" + +msgid "suggest edit" +msgstr "編集を提案する" + +msgid "open issue" +msgstr "未解決の問題" + +msgid "Launch" +msgstr "起動" + +msgid "Print to PDF" +msgstr "PDFに印刷" + +msgid "By the" +msgstr "によって" + +msgid "Last updated on" +msgstr "最終更新日" + +msgid "Download source file" +msgstr "ソースファイルをダウンロード" + +msgid "Download this page" +msgstr "このページをダウンロード" diff --git a/main/_static/locales/ko/LC_MESSAGES/booktheme.mo b/main/_static/locales/ko/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..06c7ec9 Binary files /dev/null and b/main/_static/locales/ko/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/ko/LC_MESSAGES/booktheme.po b/main/_static/locales/ko/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..6ee3d78 --- /dev/null +++ b/main/_static/locales/ko/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ko\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "테마별" + +msgid "Open an issue" +msgstr "이슈 열기" + +msgid "Contents" +msgstr "내용" + +msgid "Download notebook file" +msgstr "노트북 파일 다운로드" + +msgid "Sphinx Book Theme" +msgstr "스핑크스 도서 테마" + +msgid "Fullscreen mode" +msgstr "전체 화면으로보기" + +msgid "Edit this page" +msgstr "이 페이지 편집" + +msgid "By" +msgstr "으로" + +msgid "Copyright" +msgstr "저작권" + +msgid "Source repository" +msgstr "소스 저장소" + +msgid "previous page" +msgstr "이전 페이지" + +msgid "next page" +msgstr "다음 페이지" + +msgid "Toggle navigation" +msgstr "탐색 전환" + +msgid "repository" +msgstr "저장소" + +msgid "suggest edit" +msgstr "편집 제안" + +msgid "open issue" +msgstr "열린 문제" + +msgid "Launch" +msgstr "시작하다" + +msgid "Print to PDF" +msgstr "PDF로 인쇄" + +msgid "By the" +msgstr "에 의해" + +msgid "Last updated on" +msgstr "마지막 업데이트" + +msgid "Download source file" +msgstr "소스 파일 다운로드" + +msgid "Download this page" +msgstr "이 페이지 다운로드" diff --git a/main/_static/locales/lt/LC_MESSAGES/booktheme.mo b/main/_static/locales/lt/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..4468ba0 Binary files /dev/null and b/main/_static/locales/lt/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/lt/LC_MESSAGES/booktheme.po b/main/_static/locales/lt/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..01be267 --- /dev/null +++ b/main/_static/locales/lt/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: lt\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema" + +msgid "Open an issue" +msgstr "Atidarykite problemą" + +msgid "Contents" +msgstr "Turinys" + +msgid "Download notebook file" +msgstr "Atsisiųsti nešiojamojo kompiuterio failą" + +msgid "Sphinx Book Theme" +msgstr "Sfinkso knygos tema" + +msgid "Fullscreen mode" +msgstr "Pilno ekrano režimas" + +msgid "Edit this page" +msgstr "Redaguoti šį puslapį" + +msgid "By" +msgstr "Iki" + +msgid "Copyright" +msgstr "Autorių teisės" + +msgid "Source repository" +msgstr "Šaltinio saugykla" + +msgid "previous page" +msgstr "Ankstesnis puslapis" + +msgid "next page" +msgstr "Kitas puslapis" + +msgid "Toggle navigation" +msgstr "Perjungti naršymą" + +msgid "repository" +msgstr "saugykla" + +msgid "suggest edit" +msgstr "pasiūlyti redaguoti" + +msgid "open issue" +msgstr "atviras klausimas" + +msgid "Launch" +msgstr "Paleiskite" + +msgid "Print to PDF" +msgstr "Spausdinti į PDF" + +msgid "By the" +msgstr "Prie" + +msgid "Last updated on" +msgstr "Paskutinį kartą atnaujinta" + +msgid "Download source file" +msgstr "Atsisiųsti šaltinio failą" + +msgid "Download this page" +msgstr "Atsisiųskite šį puslapį" diff --git a/main/_static/locales/lv/LC_MESSAGES/booktheme.mo b/main/_static/locales/lv/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..74aa4d8 Binary files /dev/null and b/main/_static/locales/lv/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/lv/LC_MESSAGES/booktheme.po b/main/_static/locales/lv/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..993a1e4 --- /dev/null +++ b/main/_static/locales/lv/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: lv\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Autora tēma" + +msgid "Open an issue" +msgstr "Atveriet problēmu" + +msgid "Contents" +msgstr "Saturs" + +msgid "Download notebook file" +msgstr "Lejupielādēt piezīmju grāmatiņu" + +msgid "Sphinx Book Theme" +msgstr "Sfinksa grāmatas tēma" + +msgid "Fullscreen mode" +msgstr "Pilnekrāna režīms" + +msgid "Edit this page" +msgstr "Rediģēt šo lapu" + +msgid "By" +msgstr "Autors" + +msgid "Copyright" +msgstr "Autortiesības" + +msgid "Source repository" +msgstr "Avota krātuve" + +msgid "previous page" +msgstr "iepriekšējā lapa" + +msgid "next page" +msgstr "nākamā lapaspuse" + +msgid "Toggle navigation" +msgstr "Pārslēgt navigāciju" + +msgid "repository" +msgstr "krātuve" + +msgid "suggest edit" +msgstr "ieteikt rediģēt" + +msgid "open issue" +msgstr "atklāts jautājums" + +msgid "Launch" +msgstr "Uzsākt" + +msgid "Print to PDF" +msgstr "Drukāt PDF formātā" + +msgid "By the" +msgstr "Ar" + +msgid "Last updated on" +msgstr "Pēdējoreiz atjaunināts" + +msgid "Download source file" +msgstr "Lejupielādēt avota failu" + +msgid "Download this page" +msgstr "Lejupielādējiet šo lapu" diff --git a/main/_static/locales/ml/LC_MESSAGES/booktheme.mo b/main/_static/locales/ml/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..2736e8f Binary files /dev/null and b/main/_static/locales/ml/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/ml/LC_MESSAGES/booktheme.po b/main/_static/locales/ml/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..81daf7c --- /dev/null +++ b/main/_static/locales/ml/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ml\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "പ്രമേയം" + +msgid "Open an issue" +msgstr "ഒരു പ്രശ്നം തുറക്കുക" + +msgid "Download notebook file" +msgstr "നോട്ട്ബുക്ക് ഫയൽ ഡൺലോഡ് ചെയ്യുക" + +msgid "Sphinx Book Theme" +msgstr "സ്ഫിങ്ക്സ് പുസ്തക തീം" + +msgid "Edit this page" +msgstr "ഈ പേജ് എഡിറ്റുചെയ്യുക" + +msgid "By" +msgstr "എഴുതിയത്" + +msgid "Copyright" +msgstr "പകർപ്പവകാശം" + +msgid "Source repository" +msgstr "ഉറവിട ശേഖരം" + +msgid "previous page" +msgstr "മുൻപത്തെ താൾ" + +msgid "next page" +msgstr "അടുത്ത പേജ്" + +msgid "Toggle navigation" +msgstr "നാവിഗേഷൻ ടോഗിൾ ചെയ്യുക" + +msgid "suggest edit" +msgstr "എഡിറ്റുചെയ്യാൻ നിർദ്ദേശിക്കുക" + +msgid "open issue" +msgstr "തുറന്ന പ്രശ്നം" + +msgid "Launch" +msgstr "സമാരംഭിക്കുക" + +msgid "Print to PDF" +msgstr "PDF- ലേക്ക് പ്രിന്റുചെയ്യുക" + +msgid "By the" +msgstr "എഴുതിയത്" + +msgid "Last updated on" +msgstr "അവസാനം അപ്‌ഡേറ്റുചെയ്‌തത്" + +msgid "Download source file" +msgstr "ഉറവിട ഫയൽ ഡൗൺലോഡുചെയ്യുക" + +msgid "Download this page" +msgstr "ഈ പേജ് ഡൗൺലോഡുചെയ്യുക" diff --git a/main/_static/locales/mr/LC_MESSAGES/booktheme.mo b/main/_static/locales/mr/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..fe53010 Binary files /dev/null and b/main/_static/locales/mr/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/mr/LC_MESSAGES/booktheme.po b/main/_static/locales/mr/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..fd857bf --- /dev/null +++ b/main/_static/locales/mr/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: mr\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "द्वारा थीम" + +msgid "Open an issue" +msgstr "एक मुद्दा उघडा" + +msgid "Download notebook file" +msgstr "नोटबुक फाईल डाउनलोड करा" + +msgid "Sphinx Book Theme" +msgstr "स्फिंक्स बुक थीम" + +msgid "Edit this page" +msgstr "हे पृष्ठ संपादित करा" + +msgid "By" +msgstr "द्वारा" + +msgid "Copyright" +msgstr "कॉपीराइट" + +msgid "Source repository" +msgstr "स्त्रोत भांडार" + +msgid "previous page" +msgstr "मागील पान" + +msgid "next page" +msgstr "पुढील पृष्ठ" + +msgid "Toggle navigation" +msgstr "नेव्हिगेशन टॉगल करा" + +msgid "suggest edit" +msgstr "संपादन सुचवा" + +msgid "open issue" +msgstr "खुला मुद्दा" + +msgid "Launch" +msgstr "लाँच करा" + +msgid "Print to PDF" +msgstr "पीडीएफवर मुद्रित करा" + +msgid "By the" +msgstr "द्वारा" + +msgid "Last updated on" +msgstr "अखेरचे अद्यतनित" + +msgid "Download source file" +msgstr "स्त्रोत फाइल डाउनलोड करा" + +msgid "Download this page" +msgstr "हे पृष्ठ डाउनलोड करा" diff --git a/main/_static/locales/ms/LC_MESSAGES/booktheme.mo b/main/_static/locales/ms/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..f02603f Binary files /dev/null and b/main/_static/locales/ms/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/ms/LC_MESSAGES/booktheme.po b/main/_static/locales/ms/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..b616d70 --- /dev/null +++ b/main/_static/locales/ms/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ms\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema oleh" + +msgid "Open an issue" +msgstr "Buka masalah" + +msgid "Download notebook file" +msgstr "Muat turun fail buku nota" + +msgid "Sphinx Book Theme" +msgstr "Tema Buku Sphinx" + +msgid "Edit this page" +msgstr "Edit halaman ini" + +msgid "By" +msgstr "Oleh" + +msgid "Copyright" +msgstr "hak cipta" + +msgid "Source repository" +msgstr "Repositori sumber" + +msgid "previous page" +msgstr "halaman sebelumnya" + +msgid "next page" +msgstr "muka surat seterusnya" + +msgid "Toggle navigation" +msgstr "Togol navigasi" + +msgid "suggest edit" +msgstr "cadangkan edit" + +msgid "open issue" +msgstr "isu terbuka" + +msgid "Launch" +msgstr "Lancarkan" + +msgid "Print to PDF" +msgstr "Cetak ke PDF" + +msgid "By the" +msgstr "Oleh" + +msgid "Last updated on" +msgstr "Terakhir dikemas kini pada" + +msgid "Download source file" +msgstr "Muat turun fail sumber" + +msgid "Download this page" +msgstr "Muat turun halaman ini" diff --git a/main/_static/locales/nl/LC_MESSAGES/booktheme.mo b/main/_static/locales/nl/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..e59e7ec Binary files /dev/null and b/main/_static/locales/nl/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/nl/LC_MESSAGES/booktheme.po b/main/_static/locales/nl/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..f16f4bc --- /dev/null +++ b/main/_static/locales/nl/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: nl\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Thema door de" + +msgid "Open an issue" +msgstr "Open een probleem" + +msgid "Contents" +msgstr "Inhoud" + +msgid "Download notebook file" +msgstr "Download notebookbestand" + +msgid "Sphinx Book Theme" +msgstr "Sphinx-boekthema" + +msgid "Fullscreen mode" +msgstr "Volledig scherm" + +msgid "Edit this page" +msgstr "bewerk deze pagina" + +msgid "By" +msgstr "Door" + +msgid "Copyright" +msgstr "auteursrechten" + +msgid "Source repository" +msgstr "Bronopslagplaats" + +msgid "previous page" +msgstr "vorige pagina" + +msgid "next page" +msgstr "volgende bladzijde" + +msgid "Toggle navigation" +msgstr "Schakel navigatie" + +msgid "repository" +msgstr "repository" + +msgid "suggest edit" +msgstr "suggereren bewerken" + +msgid "open issue" +msgstr "open probleem" + +msgid "Launch" +msgstr "Lancering" + +msgid "Print to PDF" +msgstr "Afdrukken naar pdf" + +msgid "By the" +msgstr "Door de" + +msgid "Last updated on" +msgstr "Laatst geupdate op" + +msgid "Download source file" +msgstr "Download het bronbestand" + +msgid "Download this page" +msgstr "Download deze pagina" diff --git a/main/_static/locales/no/LC_MESSAGES/booktheme.mo b/main/_static/locales/no/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..6cd15c8 Binary files /dev/null and b/main/_static/locales/no/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/no/LC_MESSAGES/booktheme.po b/main/_static/locales/no/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..b1d304e --- /dev/null +++ b/main/_static/locales/no/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: no\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema av" + +msgid "Open an issue" +msgstr "Åpne et problem" + +msgid "Contents" +msgstr "Innhold" + +msgid "Download notebook file" +msgstr "Last ned notatbokfilen" + +msgid "Sphinx Book Theme" +msgstr "Sphinx boktema" + +msgid "Fullscreen mode" +msgstr "Fullskjerm-modus" + +msgid "Edit this page" +msgstr "Rediger denne siden" + +msgid "By" +msgstr "Av" + +msgid "Copyright" +msgstr "opphavsrett" + +msgid "Source repository" +msgstr "Kildedepot" + +msgid "previous page" +msgstr "forrige side" + +msgid "next page" +msgstr "neste side" + +msgid "Toggle navigation" +msgstr "Bytt navigasjon" + +msgid "repository" +msgstr "oppbevaringssted" + +msgid "suggest edit" +msgstr "foreslå redigering" + +msgid "open issue" +msgstr "åpent nummer" + +msgid "Launch" +msgstr "Start" + +msgid "Print to PDF" +msgstr "Skriv ut til PDF" + +msgid "By the" +msgstr "Ved" + +msgid "Last updated on" +msgstr "Sist oppdatert den" + +msgid "Download source file" +msgstr "Last ned kildefilen" + +msgid "Download this page" +msgstr "Last ned denne siden" diff --git a/main/_static/locales/pl/LC_MESSAGES/booktheme.mo b/main/_static/locales/pl/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..9ebb584 Binary files /dev/null and b/main/_static/locales/pl/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/pl/LC_MESSAGES/booktheme.po b/main/_static/locales/pl/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..80d2c89 --- /dev/null +++ b/main/_static/locales/pl/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pl\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Motyw autorstwa" + +msgid "Open an issue" +msgstr "Otwórz problem" + +msgid "Contents" +msgstr "Zawartość" + +msgid "Download notebook file" +msgstr "Pobierz plik notatnika" + +msgid "Sphinx Book Theme" +msgstr "Motyw książki Sphinx" + +msgid "Fullscreen mode" +msgstr "Pełny ekran" + +msgid "Edit this page" +msgstr "Edytuj tę strone" + +msgid "By" +msgstr "Przez" + +msgid "Copyright" +msgstr "prawa autorskie" + +msgid "Source repository" +msgstr "Repozytorium źródłowe" + +msgid "previous page" +msgstr "Poprzednia strona" + +msgid "next page" +msgstr "Następna strona" + +msgid "Toggle navigation" +msgstr "Przełącz nawigację" + +msgid "repository" +msgstr "magazyn" + +msgid "suggest edit" +msgstr "zaproponuj edycję" + +msgid "open issue" +msgstr "otwarty problem" + +msgid "Launch" +msgstr "Uruchomić" + +msgid "Print to PDF" +msgstr "Drukuj do PDF" + +msgid "By the" +msgstr "Przez" + +msgid "Last updated on" +msgstr "Ostatnia aktualizacja" + +msgid "Download source file" +msgstr "Pobierz plik źródłowy" + +msgid "Download this page" +msgstr "Pobierz tę stronę" diff --git a/main/_static/locales/pt/LC_MESSAGES/booktheme.mo b/main/_static/locales/pt/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..d0ddb87 Binary files /dev/null and b/main/_static/locales/pt/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/pt/LC_MESSAGES/booktheme.po b/main/_static/locales/pt/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..45ac847 --- /dev/null +++ b/main/_static/locales/pt/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pt\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema por" + +msgid "Open an issue" +msgstr "Abra um problema" + +msgid "Contents" +msgstr "Conteúdo" + +msgid "Download notebook file" +msgstr "Baixar arquivo de notebook" + +msgid "Sphinx Book Theme" +msgstr "Tema do livro Sphinx" + +msgid "Fullscreen mode" +msgstr "Modo tela cheia" + +msgid "Edit this page" +msgstr "Edite essa página" + +msgid "By" +msgstr "De" + +msgid "Copyright" +msgstr "direito autoral" + +msgid "Source repository" +msgstr "Repositório fonte" + +msgid "previous page" +msgstr "página anterior" + +msgid "next page" +msgstr "próxima página" + +msgid "Toggle navigation" +msgstr "Alternar de navegação" + +msgid "repository" +msgstr "repositório" + +msgid "suggest edit" +msgstr "sugerir edição" + +msgid "open issue" +msgstr "questão aberta" + +msgid "Launch" +msgstr "Lançamento" + +msgid "Print to PDF" +msgstr "Imprimir em PDF" + +msgid "By the" +msgstr "Pelo" + +msgid "Last updated on" +msgstr "Última atualização em" + +msgid "Download source file" +msgstr "Baixar arquivo fonte" + +msgid "Download this page" +msgstr "Baixe esta página" diff --git a/main/_static/locales/ro/LC_MESSAGES/booktheme.mo b/main/_static/locales/ro/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..3c36ab1 Binary files /dev/null and b/main/_static/locales/ro/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/ro/LC_MESSAGES/booktheme.po b/main/_static/locales/ro/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..532b3b8 --- /dev/null +++ b/main/_static/locales/ro/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ro\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema de" + +msgid "Open an issue" +msgstr "Deschideți o problemă" + +msgid "Contents" +msgstr "Cuprins" + +msgid "Download notebook file" +msgstr "Descărcați fișierul notebook" + +msgid "Sphinx Book Theme" +msgstr "Tema Sphinx Book" + +msgid "Fullscreen mode" +msgstr "Modul ecran întreg" + +msgid "Edit this page" +msgstr "Editați această pagină" + +msgid "By" +msgstr "De" + +msgid "Copyright" +msgstr "Drepturi de autor" + +msgid "Source repository" +msgstr "Depozit sursă" + +msgid "previous page" +msgstr "pagina anterioară" + +msgid "next page" +msgstr "pagina următoare" + +msgid "Toggle navigation" +msgstr "Comutare navigare" + +msgid "repository" +msgstr "repertoriu" + +msgid "suggest edit" +msgstr "sugerează editare" + +msgid "open issue" +msgstr "problema deschisă" + +msgid "Launch" +msgstr "Lansa" + +msgid "Print to PDF" +msgstr "Imprimați în PDF" + +msgid "By the" +msgstr "Langa" + +msgid "Last updated on" +msgstr "Ultima actualizare la" + +msgid "Download source file" +msgstr "Descărcați fișierul sursă" + +msgid "Download this page" +msgstr "Descarcă această pagină" diff --git a/main/_static/locales/ru/LC_MESSAGES/booktheme.mo b/main/_static/locales/ru/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..6b8ca41 Binary files /dev/null and b/main/_static/locales/ru/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/ru/LC_MESSAGES/booktheme.po b/main/_static/locales/ru/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..b718b48 --- /dev/null +++ b/main/_static/locales/ru/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ru\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Тема от" + +msgid "Open an issue" +msgstr "Открыть вопрос" + +msgid "Contents" +msgstr "Содержание" + +msgid "Download notebook file" +msgstr "Скачать файл записной книжки" + +msgid "Sphinx Book Theme" +msgstr "Тема книги Сфинкс" + +msgid "Fullscreen mode" +msgstr "Полноэкранный режим" + +msgid "Edit this page" +msgstr "Редактировать эту страницу" + +msgid "By" +msgstr "По" + +msgid "Copyright" +msgstr "авторское право" + +msgid "Source repository" +msgstr "Исходный репозиторий" + +msgid "previous page" +msgstr "Предыдущая страница" + +msgid "next page" +msgstr "Следующая страница" + +msgid "Toggle navigation" +msgstr "Переключить навигацию" + +msgid "repository" +msgstr "хранилище" + +msgid "suggest edit" +msgstr "предложить редактировать" + +msgid "open issue" +msgstr "открытый вопрос" + +msgid "Launch" +msgstr "Запуск" + +msgid "Print to PDF" +msgstr "Распечатать в PDF" + +msgid "By the" +msgstr "Посредством" + +msgid "Last updated on" +msgstr "Последнее обновление" + +msgid "Download source file" +msgstr "Скачать исходный файл" + +msgid "Download this page" +msgstr "Загрузите эту страницу" diff --git a/main/_static/locales/sk/LC_MESSAGES/booktheme.mo b/main/_static/locales/sk/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..59bd0dd Binary files /dev/null and b/main/_static/locales/sk/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/sk/LC_MESSAGES/booktheme.po b/main/_static/locales/sk/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..f6c423b --- /dev/null +++ b/main/_static/locales/sk/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: sk\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Téma od" + +msgid "Open an issue" +msgstr "Otvorte problém" + +msgid "Contents" +msgstr "Obsah" + +msgid "Download notebook file" +msgstr "Stiahnite si zošit" + +msgid "Sphinx Book Theme" +msgstr "Téma knihy Sfinga" + +msgid "Fullscreen mode" +msgstr "Režim celej obrazovky" + +msgid "Edit this page" +msgstr "Upraviť túto stránku" + +msgid "By" +msgstr "Autor:" + +msgid "Copyright" +msgstr "Autorské práva" + +msgid "Source repository" +msgstr "Zdrojové úložisko" + +msgid "previous page" +msgstr "predchádzajúca strana" + +msgid "next page" +msgstr "ďalšia strana" + +msgid "Toggle navigation" +msgstr "Prepnúť navigáciu" + +msgid "repository" +msgstr "Úložisko" + +msgid "suggest edit" +msgstr "navrhnúť úpravu" + +msgid "open issue" +msgstr "otvorené vydanie" + +msgid "Launch" +msgstr "Spustiť" + +msgid "Print to PDF" +msgstr "Tlač do PDF" + +msgid "By the" +msgstr "Podľa" + +msgid "Last updated on" +msgstr "Posledná aktualizácia dňa" + +msgid "Download source file" +msgstr "Stiahnite si zdrojový súbor" + +msgid "Download this page" +msgstr "Stiahnite si túto stránku" diff --git a/main/_static/locales/sl/LC_MESSAGES/booktheme.mo b/main/_static/locales/sl/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..87bf26d Binary files /dev/null and b/main/_static/locales/sl/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/sl/LC_MESSAGES/booktheme.po b/main/_static/locales/sl/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..9822dc5 --- /dev/null +++ b/main/_static/locales/sl/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: sl\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema avtorja" + +msgid "Open an issue" +msgstr "Odprite številko" + +msgid "Contents" +msgstr "Vsebina" + +msgid "Download notebook file" +msgstr "Prenesite datoteko zvezka" + +msgid "Sphinx Book Theme" +msgstr "Tema knjige Sphinx" + +msgid "Fullscreen mode" +msgstr "Celozaslonski način" + +msgid "Edit this page" +msgstr "Uredite to stran" + +msgid "By" +msgstr "Avtor" + +msgid "Copyright" +msgstr "avtorske pravice" + +msgid "Source repository" +msgstr "Izvorno skladišče" + +msgid "previous page" +msgstr "Prejšnja stran" + +msgid "next page" +msgstr "Naslednja stran" + +msgid "Toggle navigation" +msgstr "Preklopi navigacijo" + +msgid "repository" +msgstr "odlagališče" + +msgid "suggest edit" +msgstr "predlagajte urejanje" + +msgid "open issue" +msgstr "odprto vprašanje" + +msgid "Launch" +msgstr "Kosilo" + +msgid "Print to PDF" +msgstr "Natisni v PDF" + +msgid "By the" +msgstr "Avtor" + +msgid "Last updated on" +msgstr "Nazadnje posodobljeno dne" + +msgid "Download source file" +msgstr "Prenesite izvorno datoteko" + +msgid "Download this page" +msgstr "Prenesite to stran" diff --git a/main/_static/locales/sr/LC_MESSAGES/booktheme.mo b/main/_static/locales/sr/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..ec740f4 Binary files /dev/null and b/main/_static/locales/sr/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/sr/LC_MESSAGES/booktheme.po b/main/_static/locales/sr/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..e809230 --- /dev/null +++ b/main/_static/locales/sr/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: sr\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Тхеме би" + +msgid "Open an issue" +msgstr "Отворите издање" + +msgid "Contents" +msgstr "Садржај" + +msgid "Download notebook file" +msgstr "Преузмите датотеку бележнице" + +msgid "Sphinx Book Theme" +msgstr "Тема књиге Спхинк" + +msgid "Fullscreen mode" +msgstr "Режим целог екрана" + +msgid "Edit this page" +msgstr "Уредите ову страницу" + +msgid "By" +msgstr "Од стране" + +msgid "Copyright" +msgstr "Ауторско право" + +msgid "Source repository" +msgstr "Изворно спремиште" + +msgid "previous page" +msgstr "Претходна страница" + +msgid "next page" +msgstr "Следећа страна" + +msgid "Toggle navigation" +msgstr "Укључи / искључи навигацију" + +msgid "repository" +msgstr "спремиште" + +msgid "suggest edit" +msgstr "предложи уређивање" + +msgid "open issue" +msgstr "отворено издање" + +msgid "Launch" +msgstr "Лансирање" + +msgid "Print to PDF" +msgstr "Испис у ПДФ" + +msgid "By the" +msgstr "Од" + +msgid "Last updated on" +msgstr "Последње ажурирање" + +msgid "Download source file" +msgstr "Преузми изворну датотеку" + +msgid "Download this page" +msgstr "Преузмите ову страницу" diff --git a/main/_static/locales/sv/LC_MESSAGES/booktheme.mo b/main/_static/locales/sv/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..b07dc76 Binary files /dev/null and b/main/_static/locales/sv/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/sv/LC_MESSAGES/booktheme.po b/main/_static/locales/sv/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..2421b00 --- /dev/null +++ b/main/_static/locales/sv/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: sv\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema av" + +msgid "Open an issue" +msgstr "Öppna en problemrapport" + +msgid "Contents" +msgstr "Innehåll" + +msgid "Download notebook file" +msgstr "Ladda ner notebook-fil" + +msgid "Sphinx Book Theme" +msgstr "Sphinx Boktema" + +msgid "Fullscreen mode" +msgstr "Fullskärmsläge" + +msgid "Edit this page" +msgstr "Redigera den här sidan" + +msgid "By" +msgstr "Av" + +msgid "Copyright" +msgstr "Upphovsrätt" + +msgid "Source repository" +msgstr "Källkodsrepositorium" + +msgid "previous page" +msgstr "föregående sida" + +msgid "next page" +msgstr "nästa sida" + +msgid "Toggle navigation" +msgstr "Växla navigering" + +msgid "repository" +msgstr "repositorium" + +msgid "suggest edit" +msgstr "föreslå ändring" + +msgid "open issue" +msgstr "öppna problemrapport" + +msgid "Launch" +msgstr "Öppna" + +msgid "Print to PDF" +msgstr "Skriv ut till PDF" + +msgid "By the" +msgstr "Av den" + +msgid "Last updated on" +msgstr "Senast uppdaterad den" + +msgid "Download source file" +msgstr "Ladda ner källfil" + +msgid "Download this page" +msgstr "Ladda ner den här sidan" diff --git a/main/_static/locales/ta/LC_MESSAGES/booktheme.mo b/main/_static/locales/ta/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..29f52e1 Binary files /dev/null and b/main/_static/locales/ta/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/ta/LC_MESSAGES/booktheme.po b/main/_static/locales/ta/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..500042f --- /dev/null +++ b/main/_static/locales/ta/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ta\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "வழங்கிய தீம்" + +msgid "Open an issue" +msgstr "சிக்கலைத் திறக்கவும்" + +msgid "Download notebook file" +msgstr "நோட்புக் கோப்பைப் பதிவிறக்கவும்" + +msgid "Sphinx Book Theme" +msgstr "ஸ்பிங்க்ஸ் புத்தக தீம்" + +msgid "Edit this page" +msgstr "இந்தப் பக்கத்தைத் திருத்தவும்" + +msgid "By" +msgstr "வழங்கியவர்" + +msgid "Copyright" +msgstr "பதிப்புரிமை" + +msgid "Source repository" +msgstr "மூல களஞ்சியம்" + +msgid "previous page" +msgstr "முந்தைய பக்கம்" + +msgid "next page" +msgstr "அடுத்த பக்கம்" + +msgid "Toggle navigation" +msgstr "வழிசெலுத்தலை நிலைமாற்று" + +msgid "suggest edit" +msgstr "திருத்த பரிந்துரைக்கவும்" + +msgid "open issue" +msgstr "திறந்த பிரச்சினை" + +msgid "Launch" +msgstr "தொடங்க" + +msgid "Print to PDF" +msgstr "PDF இல் அச்சிடுக" + +msgid "By the" +msgstr "மூலம்" + +msgid "Last updated on" +msgstr "கடைசியாக புதுப்பிக்கப்பட்டது" + +msgid "Download source file" +msgstr "மூல கோப்பைப் பதிவிறக்குக" + +msgid "Download this page" +msgstr "இந்தப் பக்கத்தைப் பதிவிறக்கவும்" diff --git a/main/_static/locales/te/LC_MESSAGES/booktheme.mo b/main/_static/locales/te/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..0a5f4b4 Binary files /dev/null and b/main/_static/locales/te/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/te/LC_MESSAGES/booktheme.po b/main/_static/locales/te/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..b1afebb --- /dev/null +++ b/main/_static/locales/te/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: te\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "ద్వారా థీమ్" + +msgid "Open an issue" +msgstr "సమస్యను తెరవండి" + +msgid "Download notebook file" +msgstr "నోట్బుక్ ఫైల్ను డౌన్లోడ్ చేయండి" + +msgid "Sphinx Book Theme" +msgstr "సింహిక పుస్తక థీమ్" + +msgid "Edit this page" +msgstr "ఈ పేజీని సవరించండి" + +msgid "By" +msgstr "ద్వారా" + +msgid "Copyright" +msgstr "కాపీరైట్" + +msgid "Source repository" +msgstr "మూల రిపోజిటరీ" + +msgid "previous page" +msgstr "ముందు పేజి" + +msgid "next page" +msgstr "తరువాతి పేజీ" + +msgid "Toggle navigation" +msgstr "నావిగేషన్‌ను టోగుల్ చేయండి" + +msgid "suggest edit" +msgstr "సవరించమని సూచించండి" + +msgid "open issue" +msgstr "ఓపెన్ ఇష్యూ" + +msgid "Launch" +msgstr "ప్రారంభించండి" + +msgid "Print to PDF" +msgstr "PDF కి ముద్రించండి" + +msgid "By the" +msgstr "ద్వారా" + +msgid "Last updated on" +msgstr "చివరిగా నవీకరించబడింది" + +msgid "Download source file" +msgstr "మూల ఫైల్‌ను డౌన్‌లోడ్ చేయండి" + +msgid "Download this page" +msgstr "ఈ పేజీని డౌన్‌లోడ్ చేయండి" diff --git a/main/_static/locales/tg/LC_MESSAGES/booktheme.mo b/main/_static/locales/tg/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..b21c6c6 Binary files /dev/null and b/main/_static/locales/tg/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/tg/LC_MESSAGES/booktheme.po b/main/_static/locales/tg/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..29b8237 --- /dev/null +++ b/main/_static/locales/tg/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: tg\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Мавзӯъи аз" + +msgid "Open an issue" +msgstr "Масъаларо кушоед" + +msgid "Contents" +msgstr "Мундариҷа" + +msgid "Download notebook file" +msgstr "Файли дафтарро зеркашӣ кунед" + +msgid "Sphinx Book Theme" +msgstr "Сфинкс Мавзӯи китоб" + +msgid "Fullscreen mode" +msgstr "Ҳолати экрани пурра" + +msgid "Edit this page" +msgstr "Ин саҳифаро таҳрир кунед" + +msgid "By" +msgstr "Бо" + +msgid "Copyright" +msgstr "Ҳуқуқи муаллиф" + +msgid "Source repository" +msgstr "Анбори манбаъ" + +msgid "previous page" +msgstr "саҳифаи қаблӣ" + +msgid "next page" +msgstr "саҳифаи оянда" + +msgid "Toggle navigation" +msgstr "Гузаришро иваз кунед" + +msgid "repository" +msgstr "анбор" + +msgid "suggest edit" +msgstr "пешниҳод вироиш" + +msgid "open issue" +msgstr "барориши кушод" + +msgid "Launch" +msgstr "Оғоз" + +msgid "Print to PDF" +msgstr "Чоп ба PDF" + +msgid "By the" +msgstr "Бо" + +msgid "Last updated on" +msgstr "Last навсозӣ дар" + +msgid "Download source file" +msgstr "Файли манбаъро зеркашӣ кунед" + +msgid "Download this page" +msgstr "Ин саҳифаро зеркашӣ кунед" diff --git a/main/_static/locales/th/LC_MESSAGES/booktheme.mo b/main/_static/locales/th/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..abede98 Binary files /dev/null and b/main/_static/locales/th/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/th/LC_MESSAGES/booktheme.po b/main/_static/locales/th/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..ac65ee0 --- /dev/null +++ b/main/_static/locales/th/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: th\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "ธีมโดย" + +msgid "Open an issue" +msgstr "เปิดปัญหา" + +msgid "Contents" +msgstr "สารบัญ" + +msgid "Download notebook file" +msgstr "ดาวน์โหลดไฟล์สมุดบันทึก" + +msgid "Sphinx Book Theme" +msgstr "ธีมหนังสือสฟิงซ์" + +msgid "Fullscreen mode" +msgstr "โหมดเต็มหน้าจอ" + +msgid "Edit this page" +msgstr "แก้ไขหน้านี้" + +msgid "By" +msgstr "โดย" + +msgid "Copyright" +msgstr "ลิขสิทธิ์" + +msgid "Source repository" +msgstr "ที่เก็บซอร์ส" + +msgid "previous page" +msgstr "หน้าที่แล้ว" + +msgid "next page" +msgstr "หน้าต่อไป" + +msgid "Toggle navigation" +msgstr "ไม่ต้องสลับช่องทาง" + +msgid "repository" +msgstr "ที่เก็บ" + +msgid "suggest edit" +msgstr "แนะนำแก้ไข" + +msgid "open issue" +msgstr "เปิดปัญหา" + +msgid "Launch" +msgstr "เปิด" + +msgid "Print to PDF" +msgstr "พิมพ์เป็น PDF" + +msgid "By the" +msgstr "โดย" + +msgid "Last updated on" +msgstr "ปรับปรุงล่าสุดเมื่อ" + +msgid "Download source file" +msgstr "ดาวน์โหลดไฟล์ต้นฉบับ" + +msgid "Download this page" +msgstr "ดาวน์โหลดหน้านี้" diff --git a/main/_static/locales/tl/LC_MESSAGES/booktheme.mo b/main/_static/locales/tl/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..8df1b73 Binary files /dev/null and b/main/_static/locales/tl/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/tl/LC_MESSAGES/booktheme.po b/main/_static/locales/tl/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..662d66c --- /dev/null +++ b/main/_static/locales/tl/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: tl\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema ng" + +msgid "Open an issue" +msgstr "Magbukas ng isyu" + +msgid "Download notebook file" +msgstr "Mag-download ng file ng notebook" + +msgid "Sphinx Book Theme" +msgstr "Tema ng Sphinx Book" + +msgid "Edit this page" +msgstr "I-edit ang pahinang ito" + +msgid "By" +msgstr "Ni" + +msgid "Copyright" +msgstr "Copyright" + +msgid "Source repository" +msgstr "Pinagmulan ng imbakan" + +msgid "previous page" +msgstr "Nakaraang pahina" + +msgid "next page" +msgstr "Susunod na pahina" + +msgid "Toggle navigation" +msgstr "I-toggle ang pag-navigate" + +msgid "suggest edit" +msgstr "iminumungkahi i-edit" + +msgid "open issue" +msgstr "bukas na isyu" + +msgid "Launch" +msgstr "Ilunsad" + +msgid "Print to PDF" +msgstr "I-print sa PDF" + +msgid "By the" +msgstr "Sa pamamagitan ng" + +msgid "Last updated on" +msgstr "Huling na-update noong" + +msgid "Download source file" +msgstr "Mag-download ng file ng pinagmulan" + +msgid "Download this page" +msgstr "I-download ang pahinang ito" diff --git a/main/_static/locales/tr/LC_MESSAGES/booktheme.mo b/main/_static/locales/tr/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..029ae18 Binary files /dev/null and b/main/_static/locales/tr/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/tr/LC_MESSAGES/booktheme.po b/main/_static/locales/tr/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..d1ae723 --- /dev/null +++ b/main/_static/locales/tr/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: tr\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tarafından tema" + +msgid "Open an issue" +msgstr "Bir sorunu açın" + +msgid "Contents" +msgstr "İçindekiler" + +msgid "Download notebook file" +msgstr "Defter dosyasını indirin" + +msgid "Sphinx Book Theme" +msgstr "Sfenks Kitap Teması" + +msgid "Fullscreen mode" +msgstr "Tam ekran modu" + +msgid "Edit this page" +msgstr "Bu sayfayı düzenle" + +msgid "By" +msgstr "Tarafından" + +msgid "Copyright" +msgstr "Telif hakkı" + +msgid "Source repository" +msgstr "Kaynak kod deposu" + +msgid "previous page" +msgstr "önceki sayfa" + +msgid "next page" +msgstr "sonraki Sayfa" + +msgid "Toggle navigation" +msgstr "Gezinmeyi değiştir" + +msgid "repository" +msgstr "depo" + +msgid "suggest edit" +msgstr "düzenleme öner" + +msgid "open issue" +msgstr "Açık konu" + +msgid "Launch" +msgstr "Başlatmak" + +msgid "Print to PDF" +msgstr "PDF olarak yazdır" + +msgid "By the" +msgstr "Tarafından" + +msgid "Last updated on" +msgstr "Son güncelleme tarihi" + +msgid "Download source file" +msgstr "Kaynak dosyayı indirin" + +msgid "Download this page" +msgstr "Bu sayfayı indirin" diff --git a/main/_static/locales/uk/LC_MESSAGES/booktheme.mo b/main/_static/locales/uk/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..16ab789 Binary files /dev/null and b/main/_static/locales/uk/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/uk/LC_MESSAGES/booktheme.po b/main/_static/locales/uk/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..be49ab8 --- /dev/null +++ b/main/_static/locales/uk/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: uk\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Тема від" + +msgid "Open an issue" +msgstr "Відкрийте випуск" + +msgid "Contents" +msgstr "Зміст" + +msgid "Download notebook file" +msgstr "Завантажте файл блокнота" + +msgid "Sphinx Book Theme" +msgstr "Тема книги \"Сфінкс\"" + +msgid "Fullscreen mode" +msgstr "Повноекранний режим" + +msgid "Edit this page" +msgstr "Редагувати цю сторінку" + +msgid "By" +msgstr "Автор" + +msgid "Copyright" +msgstr "Авторське право" + +msgid "Source repository" +msgstr "Джерело сховища" + +msgid "previous page" +msgstr "Попередня сторінка" + +msgid "next page" +msgstr "Наступна сторінка" + +msgid "Toggle navigation" +msgstr "Переключити навігацію" + +msgid "repository" +msgstr "сховище" + +msgid "suggest edit" +msgstr "запропонувати редагувати" + +msgid "open issue" +msgstr "відкритий випуск" + +msgid "Launch" +msgstr "Запуск" + +msgid "Print to PDF" +msgstr "Друк у форматі PDF" + +msgid "By the" +msgstr "По" + +msgid "Last updated on" +msgstr "Останнє оновлення:" + +msgid "Download source file" +msgstr "Завантажити вихідний файл" + +msgid "Download this page" +msgstr "Завантажте цю сторінку" diff --git a/main/_static/locales/ur/LC_MESSAGES/booktheme.mo b/main/_static/locales/ur/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..de8c84b Binary files /dev/null and b/main/_static/locales/ur/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/ur/LC_MESSAGES/booktheme.po b/main/_static/locales/ur/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..94bcab3 --- /dev/null +++ b/main/_static/locales/ur/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ur\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "کے ذریعہ تھیم" + +msgid "Open an issue" +msgstr "ایک مسئلہ کھولیں" + +msgid "Download notebook file" +msgstr "نوٹ بک فائل ڈاؤن لوڈ کریں" + +msgid "Sphinx Book Theme" +msgstr "سپنکس بک تھیم" + +msgid "Edit this page" +msgstr "اس صفحے میں ترمیم کریں" + +msgid "By" +msgstr "بذریعہ" + +msgid "Copyright" +msgstr "کاپی رائٹ" + +msgid "Source repository" +msgstr "ماخذ ذخیرہ" + +msgid "previous page" +msgstr "سابقہ ​​صفحہ" + +msgid "next page" +msgstr "اگلا صفحہ" + +msgid "Toggle navigation" +msgstr "نیویگیشن ٹوگل کریں" + +msgid "suggest edit" +msgstr "ترمیم کی تجویز کریں" + +msgid "open issue" +msgstr "کھلا مسئلہ" + +msgid "Launch" +msgstr "لانچ کریں" + +msgid "Print to PDF" +msgstr "پی ڈی ایف پرنٹ کریں" + +msgid "By the" +msgstr "کی طرف" + +msgid "Last updated on" +msgstr "آخری بار تازہ کاری ہوئی" + +msgid "Download source file" +msgstr "سورس فائل ڈاؤن لوڈ کریں" + +msgid "Download this page" +msgstr "اس صفحے کو ڈاؤن لوڈ کریں" diff --git a/main/_static/locales/vi/LC_MESSAGES/booktheme.mo b/main/_static/locales/vi/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..2bb3255 Binary files /dev/null and b/main/_static/locales/vi/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/vi/LC_MESSAGES/booktheme.po b/main/_static/locales/vi/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..116236d --- /dev/null +++ b/main/_static/locales/vi/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: vi\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Chủ đề của" + +msgid "Open an issue" +msgstr "Mở một vấn đề" + +msgid "Contents" +msgstr "Nội dung" + +msgid "Download notebook file" +msgstr "Tải xuống tệp sổ tay" + +msgid "Sphinx Book Theme" +msgstr "Chủ đề sách nhân sư" + +msgid "Fullscreen mode" +msgstr "Chế độ toàn màn hình" + +msgid "Edit this page" +msgstr "chỉnh sửa trang này" + +msgid "By" +msgstr "Bởi" + +msgid "Copyright" +msgstr "Bản quyền" + +msgid "Source repository" +msgstr "Kho nguồn" + +msgid "previous page" +msgstr "trang trước" + +msgid "next page" +msgstr "Trang tiếp theo" + +msgid "Toggle navigation" +msgstr "Chuyển đổi điều hướng thành" + +msgid "repository" +msgstr "kho" + +msgid "suggest edit" +msgstr "đề nghị chỉnh sửa" + +msgid "open issue" +msgstr "vấn đề mở" + +msgid "Launch" +msgstr "Phóng" + +msgid "Print to PDF" +msgstr "In sang PDF" + +msgid "By the" +msgstr "Bằng" + +msgid "Last updated on" +msgstr "Cập nhật lần cuối vào" + +msgid "Download source file" +msgstr "Tải xuống tệp nguồn" + +msgid "Download this page" +msgstr "Tải xuống trang này" diff --git a/main/_static/locales/zh_CN/LC_MESSAGES/booktheme.mo b/main/_static/locales/zh_CN/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..0e3235d Binary files /dev/null and b/main/_static/locales/zh_CN/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/zh_CN/LC_MESSAGES/booktheme.po b/main/_static/locales/zh_CN/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..4f4ab57 --- /dev/null +++ b/main/_static/locales/zh_CN/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: zh_CN\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "主题作者:" + +msgid "Open an issue" +msgstr "创建议题" + +msgid "Contents" +msgstr "目录" + +msgid "Download notebook file" +msgstr "下载笔记本文件" + +msgid "Sphinx Book Theme" +msgstr "Sphinx Book 主题" + +msgid "Fullscreen mode" +msgstr "全屏模式" + +msgid "Edit this page" +msgstr "编辑此页面" + +msgid "By" +msgstr "作者:" + +msgid "Copyright" +msgstr "版权" + +msgid "Source repository" +msgstr "源码库" + +msgid "previous page" +msgstr "上一页" + +msgid "next page" +msgstr "下一页" + +msgid "Toggle navigation" +msgstr "显示或隐藏导航栏" + +msgid "repository" +msgstr "仓库" + +msgid "suggest edit" +msgstr "提出修改建议" + +msgid "open issue" +msgstr "创建议题" + +msgid "Launch" +msgstr "启动" + +msgid "Print to PDF" +msgstr "列印成 PDF" + +msgid "By the" +msgstr "作者:" + +msgid "Last updated on" +msgstr "上次更新时间:" + +msgid "Download source file" +msgstr "下载源文件" + +msgid "Download this page" +msgstr "下载此页面" diff --git a/main/_static/locales/zh_TW/LC_MESSAGES/booktheme.mo b/main/_static/locales/zh_TW/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000..9116fa9 Binary files /dev/null and b/main/_static/locales/zh_TW/LC_MESSAGES/booktheme.mo differ diff --git a/main/_static/locales/zh_TW/LC_MESSAGES/booktheme.po b/main/_static/locales/zh_TW/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000..42b43b8 --- /dev/null +++ b/main/_static/locales/zh_TW/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: zh_TW\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "佈景主題作者:" + +msgid "Open an issue" +msgstr "開啟議題" + +msgid "Contents" +msgstr "目錄" + +msgid "Download notebook file" +msgstr "下載 Notebook 檔案" + +msgid "Sphinx Book Theme" +msgstr "Sphinx Book 佈景主題" + +msgid "Fullscreen mode" +msgstr "全螢幕模式" + +msgid "Edit this page" +msgstr "編輯此頁面" + +msgid "By" +msgstr "作者:" + +msgid "Copyright" +msgstr "Copyright" + +msgid "Source repository" +msgstr "來源儲存庫" + +msgid "previous page" +msgstr "上一頁" + +msgid "next page" +msgstr "下一頁" + +msgid "Toggle navigation" +msgstr "顯示或隱藏導覽列" + +msgid "repository" +msgstr "儲存庫" + +msgid "suggest edit" +msgstr "提出修改建議" + +msgid "open issue" +msgstr "公開的問題" + +msgid "Launch" +msgstr "啟動" + +msgid "Print to PDF" +msgstr "列印成 PDF" + +msgid "By the" +msgstr "作者:" + +msgid "Last updated on" +msgstr "最後更新時間:" + +msgid "Download source file" +msgstr "下載原始檔" + +msgid "Download this page" +msgstr "下載此頁面" diff --git a/main/_static/minus.png b/main/_static/minus.png new file mode 100644 index 0000000..d96755f Binary files /dev/null and b/main/_static/minus.png differ diff --git a/main/_static/mystnb.4510f1fc1dee50b3e5859aac5469c37c29e427902b24a333a5f9fcb2f0b3ac41.css b/main/_static/mystnb.4510f1fc1dee50b3e5859aac5469c37c29e427902b24a333a5f9fcb2f0b3ac41.css new file mode 100644 index 0000000..3356631 --- /dev/null +++ b/main/_static/mystnb.4510f1fc1dee50b3e5859aac5469c37c29e427902b24a333a5f9fcb2f0b3ac41.css @@ -0,0 +1,2342 @@ +/* Variables */ +:root { + --mystnb-source-bg-color: #f7f7f7; + --mystnb-stdout-bg-color: #fcfcfc; + --mystnb-stderr-bg-color: #fdd; + --mystnb-traceback-bg-color: #fcfcfc; + --mystnb-source-border-color: #ccc; + --mystnb-source-margin-color: green; + --mystnb-stdout-border-color: #f7f7f7; + --mystnb-stderr-border-color: #f7f7f7; + --mystnb-traceback-border-color: #ffd6d6; + --mystnb-hide-prompt-opacity: 70%; + --mystnb-source-border-radius: .4em; + --mystnb-source-border-width: 1px; +} + +/* Whole cell */ +div.container.cell { + padding-left: 0; + margin-bottom: 1em; +} + +/* Removing all background formatting so we can control at the div level */ +.cell_input div.highlight, +.cell_output pre, +.cell_input pre, +.cell_output .output { + border: none; + box-shadow: none; +} + +.cell_output .output pre, +.cell_input pre { + margin: 0px; +} + +/* Input cells */ +div.cell div.cell_input, +div.cell details.above-input>summary { + padding-left: 0em; + padding-right: 0em; + border: var(--mystnb-source-border-width) var(--mystnb-source-border-color) solid; + background-color: var(--mystnb-source-bg-color); + border-left-color: var(--mystnb-source-margin-color); + border-left-width: medium; + border-radius: var(--mystnb-source-border-radius); +} + +div.cell_input>div, +div.cell_output div.output>div.highlight { + margin: 0em !important; + border: none !important; +} + +/* All cell outputs */ +.cell_output { + padding-left: 1em; + padding-right: 0em; + margin-top: 1em; +} + +/* Text outputs from cells */ +.cell_output .output.text_plain, +.cell_output .output.traceback, +.cell_output .output.stream, +.cell_output .output.stderr { + margin-top: 1em; + margin-bottom: 0em; + box-shadow: none; +} + +.cell_output .output.text_plain, +.cell_output .output.stream { + background: var(--mystnb-stdout-bg-color); + border: 1px solid var(--mystnb-stdout-border-color); +} + +.cell_output .output.stderr { + background: var(--mystnb-stderr-bg-color); + border: 1px solid var(--mystnb-stderr-border-color); +} + +.cell_output .output.traceback { + background: var(--mystnb-traceback-bg-color); + border: 1px solid var(--mystnb-traceback-border-color); +} + +/* Collapsible cell content */ +div.cell details.above-input div.cell_input { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-top: var(--mystnb-source-border-width) var(--mystnb-source-border-color) dashed; +} + +div.cell div.cell_input.above-output-prompt { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +div.cell details.above-input>summary { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + border-bottom: var(--mystnb-source-border-width) var(--mystnb-source-border-color) dashed; + padding-left: 1em; + margin-bottom: 0; +} + +div.cell details.above-output>summary { + background-color: var(--mystnb-source-bg-color); + padding-left: 1em; + padding-right: 0em; + border: var(--mystnb-source-border-width) var(--mystnb-source-border-color) solid; + border-radius: var(--mystnb-source-border-radius); + border-left-color: var(--mystnb-source-margin-color); + border-left-width: medium; +} + +div.cell details.below-input>summary { + background-color: var(--mystnb-source-bg-color); + padding-left: 1em; + padding-right: 0em; + border: var(--mystnb-source-border-width) var(--mystnb-source-border-color) solid; + border-top: none; + border-bottom-left-radius: var(--mystnb-source-border-radius); + border-bottom-right-radius: var(--mystnb-source-border-radius); + border-left-color: var(--mystnb-source-margin-color); + border-left-width: medium; +} + +div.cell details.hide>summary>span { + opacity: var(--mystnb-hide-prompt-opacity); +} + +div.cell details.hide[open]>summary>span.collapsed { + display: none; +} + +div.cell details.hide:not([open])>summary>span.expanded { + display: none; +} + +@keyframes collapsed-fade-in { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} +div.cell details.hide[open]>summary~* { + -moz-animation: collapsed-fade-in 0.3s ease-in-out; + -webkit-animation: collapsed-fade-in 0.3s ease-in-out; + animation: collapsed-fade-in 0.3s ease-in-out; +} + +/* Math align to the left */ +.cell_output .MathJax_Display { + text-align: left !important; +} + +/* Pandas tables. Pulled from the Jupyter / nbsphinx CSS */ +div.cell_output table { + border: none; + border-collapse: collapse; + border-spacing: 0; + color: black; + font-size: 1em; + table-layout: fixed; +} + +div.cell_output thead { + border-bottom: 1px solid black; + vertical-align: bottom; +} + +div.cell_output tr, +div.cell_output th, +div.cell_output td { + text-align: right; + vertical-align: middle; + padding: 0.5em 0.5em; + line-height: normal; + white-space: normal; + max-width: none; + border: none; +} + +div.cell_output th { + font-weight: bold; +} + +div.cell_output tbody tr:nth-child(odd) { + background: #f5f5f5; +} + +div.cell_output tbody tr:hover { + background: rgba(66, 165, 245, 0.2); +} + +/** source code line numbers **/ +span.linenos { + opacity: 0.5; +} + +/* Inline text from `paste` operation */ + +span.pasted-text { + font-weight: bold; +} + +span.pasted-inline img { + max-height: 2em; +} + +tbody span.pasted-inline img { + max-height: none; +} + +/* Font colors for translated ANSI escape sequences +Color values are copied from Jupyter Notebook +https://github.com/jupyter/notebook/blob/52581f8eda9b319eb0390ac77fe5903c38f81e3e/notebook/static/notebook/less/ansicolors.less#L14-L21 +Background colors from +https://nbsphinx.readthedocs.io/en/latest/code-cells.html#ANSI-Colors +*/ +div.highlight .-Color-Bold { + font-weight: bold; +} + +div.highlight .-Color[class*=-Black] { + color: #3E424D +} + +div.highlight .-Color[class*=-Red] { + color: #E75C58 +} + +div.highlight .-Color[class*=-Green] { + color: #00A250 +} + +div.highlight .-Color[class*=-Yellow] { + color: #DDB62B +} + +div.highlight .-Color[class*=-Blue] { + color: #208FFB +} + +div.highlight .-Color[class*=-Magenta] { + color: #D160C4 +} + +div.highlight .-Color[class*=-Cyan] { + color: #60C6C8 +} + +div.highlight .-Color[class*=-White] { + color: #C5C1B4 +} + +div.highlight .-Color[class*=-BGBlack] { + background-color: #3E424D +} + +div.highlight .-Color[class*=-BGRed] { + background-color: #E75C58 +} + +div.highlight .-Color[class*=-BGGreen] { + background-color: #00A250 +} + +div.highlight .-Color[class*=-BGYellow] { + background-color: #DDB62B +} + +div.highlight .-Color[class*=-BGBlue] { + background-color: #208FFB +} + +div.highlight .-Color[class*=-BGMagenta] { + background-color: #D160C4 +} + +div.highlight .-Color[class*=-BGCyan] { + background-color: #60C6C8 +} + +div.highlight .-Color[class*=-BGWhite] { + background-color: #C5C1B4 +} + +/* Font colors for 8-bit ANSI */ + +div.highlight .-Color[class*=-C0] { + color: #000000 +} + +div.highlight .-Color[class*=-BGC0] { + background-color: #000000 +} + +div.highlight .-Color[class*=-C1] { + color: #800000 +} + +div.highlight .-Color[class*=-BGC1] { + background-color: #800000 +} + +div.highlight .-Color[class*=-C2] { + color: #008000 +} + +div.highlight .-Color[class*=-BGC2] { + background-color: #008000 +} + +div.highlight .-Color[class*=-C3] { + color: #808000 +} + +div.highlight .-Color[class*=-BGC3] { + background-color: #808000 +} + +div.highlight .-Color[class*=-C4] { + color: #000080 +} + +div.highlight .-Color[class*=-BGC4] { + background-color: #000080 +} + +div.highlight .-Color[class*=-C5] { + color: #800080 +} + +div.highlight .-Color[class*=-BGC5] { + background-color: #800080 +} + +div.highlight .-Color[class*=-C6] { + color: #008080 +} + +div.highlight .-Color[class*=-BGC6] { + background-color: #008080 +} + +div.highlight .-Color[class*=-C7] { + color: #C0C0C0 +} + +div.highlight .-Color[class*=-BGC7] { + background-color: #C0C0C0 +} + +div.highlight .-Color[class*=-C8] { + color: #808080 +} + +div.highlight .-Color[class*=-BGC8] { + background-color: #808080 +} + +div.highlight .-Color[class*=-C9] { + color: #FF0000 +} + +div.highlight .-Color[class*=-BGC9] { + background-color: #FF0000 +} + +div.highlight .-Color[class*=-C10] { + color: #00FF00 +} + +div.highlight .-Color[class*=-BGC10] { + background-color: #00FF00 +} + +div.highlight .-Color[class*=-C11] { + color: #FFFF00 +} + +div.highlight .-Color[class*=-BGC11] { + background-color: #FFFF00 +} + +div.highlight .-Color[class*=-C12] { + color: #0000FF +} + +div.highlight .-Color[class*=-BGC12] { + background-color: #0000FF +} + +div.highlight .-Color[class*=-C13] { + color: #FF00FF +} + +div.highlight .-Color[class*=-BGC13] { + background-color: #FF00FF +} + +div.highlight .-Color[class*=-C14] { + color: #00FFFF +} + +div.highlight .-Color[class*=-BGC14] { + background-color: #00FFFF +} + +div.highlight .-Color[class*=-C15] { + color: #FFFFFF +} + +div.highlight .-Color[class*=-BGC15] { + background-color: #FFFFFF +} + +div.highlight .-Color[class*=-C16] { + color: #000000 +} + +div.highlight .-Color[class*=-BGC16] { + background-color: #000000 +} + +div.highlight .-Color[class*=-C17] { + color: #00005F +} + +div.highlight .-Color[class*=-BGC17] { + background-color: #00005F +} + +div.highlight .-Color[class*=-C18] { + color: #000087 +} + +div.highlight .-Color[class*=-BGC18] { + background-color: #000087 +} + +div.highlight .-Color[class*=-C19] { + color: #0000AF +} + +div.highlight .-Color[class*=-BGC19] { + background-color: #0000AF +} + +div.highlight .-Color[class*=-C20] { + color: #0000D7 +} + +div.highlight .-Color[class*=-BGC20] { + background-color: #0000D7 +} + +div.highlight .-Color[class*=-C21] { + color: #0000FF +} + +div.highlight .-Color[class*=-BGC21] { + background-color: #0000FF +} + +div.highlight .-Color[class*=-C22] { + color: #005F00 +} + +div.highlight .-Color[class*=-BGC22] { + background-color: #005F00 +} + +div.highlight .-Color[class*=-C23] { + color: #005F5F +} + +div.highlight .-Color[class*=-BGC23] { + background-color: #005F5F +} + +div.highlight .-Color[class*=-C24] { + color: #005F87 +} + +div.highlight .-Color[class*=-BGC24] { + background-color: #005F87 +} + +div.highlight .-Color[class*=-C25] { + color: #005FAF +} + +div.highlight .-Color[class*=-BGC25] { + background-color: #005FAF +} + +div.highlight .-Color[class*=-C26] { + color: #005FD7 +} + +div.highlight .-Color[class*=-BGC26] { + background-color: #005FD7 +} + +div.highlight .-Color[class*=-C27] { + color: #005FFF +} + +div.highlight .-Color[class*=-BGC27] { + background-color: #005FFF +} + +div.highlight .-Color[class*=-C28] { + color: #008700 +} + +div.highlight .-Color[class*=-BGC28] { + background-color: #008700 +} + +div.highlight .-Color[class*=-C29] { + color: #00875F +} + +div.highlight .-Color[class*=-BGC29] { + background-color: #00875F +} + +div.highlight .-Color[class*=-C30] { + color: #008787 +} + +div.highlight .-Color[class*=-BGC30] { + background-color: #008787 +} + +div.highlight .-Color[class*=-C31] { + color: #0087AF +} + +div.highlight .-Color[class*=-BGC31] { + background-color: #0087AF +} + +div.highlight .-Color[class*=-C32] { + color: #0087D7 +} + +div.highlight .-Color[class*=-BGC32] { + background-color: #0087D7 +} + +div.highlight .-Color[class*=-C33] { + color: #0087FF +} + +div.highlight .-Color[class*=-BGC33] { + background-color: #0087FF +} + +div.highlight .-Color[class*=-C34] { + color: #00AF00 +} + +div.highlight .-Color[class*=-BGC34] { + background-color: #00AF00 +} + +div.highlight .-Color[class*=-C35] { + color: #00AF5F +} + +div.highlight .-Color[class*=-BGC35] { + background-color: #00AF5F +} + +div.highlight .-Color[class*=-C36] { + color: #00AF87 +} + +div.highlight .-Color[class*=-BGC36] { + background-color: #00AF87 +} + +div.highlight .-Color[class*=-C37] { + color: #00AFAF +} + +div.highlight .-Color[class*=-BGC37] { + background-color: #00AFAF +} + +div.highlight .-Color[class*=-C38] { + color: #00AFD7 +} + +div.highlight .-Color[class*=-BGC38] { + background-color: #00AFD7 +} + +div.highlight .-Color[class*=-C39] { + color: #00AFFF +} + +div.highlight .-Color[class*=-BGC39] { + background-color: #00AFFF +} + +div.highlight .-Color[class*=-C40] { + color: #00D700 +} + +div.highlight .-Color[class*=-BGC40] { + background-color: #00D700 +} + +div.highlight .-Color[class*=-C41] { + color: #00D75F +} + +div.highlight .-Color[class*=-BGC41] { + background-color: #00D75F +} + +div.highlight .-Color[class*=-C42] { + color: #00D787 +} + +div.highlight .-Color[class*=-BGC42] { + background-color: #00D787 +} + +div.highlight .-Color[class*=-C43] { + color: #00D7AF +} + +div.highlight .-Color[class*=-BGC43] { + background-color: #00D7AF +} + +div.highlight .-Color[class*=-C44] { + color: #00D7D7 +} + +div.highlight .-Color[class*=-BGC44] { + background-color: #00D7D7 +} + +div.highlight .-Color[class*=-C45] { + color: #00D7FF +} + +div.highlight .-Color[class*=-BGC45] { + background-color: #00D7FF +} + +div.highlight .-Color[class*=-C46] { + color: #00FF00 +} + +div.highlight .-Color[class*=-BGC46] { + background-color: #00FF00 +} + +div.highlight .-Color[class*=-C47] { + color: #00FF5F +} + +div.highlight .-Color[class*=-BGC47] { + background-color: #00FF5F +} + +div.highlight .-Color[class*=-C48] { + color: #00FF87 +} + +div.highlight .-Color[class*=-BGC48] { + background-color: #00FF87 +} + +div.highlight .-Color[class*=-C49] { + color: #00FFAF +} + +div.highlight .-Color[class*=-BGC49] { + background-color: #00FFAF +} + +div.highlight .-Color[class*=-C50] { + color: #00FFD7 +} + +div.highlight .-Color[class*=-BGC50] { + background-color: #00FFD7 +} + +div.highlight .-Color[class*=-C51] { + color: #00FFFF +} + +div.highlight .-Color[class*=-BGC51] { + background-color: #00FFFF +} + +div.highlight .-Color[class*=-C52] { + color: #5F0000 +} + +div.highlight .-Color[class*=-BGC52] { + background-color: #5F0000 +} + +div.highlight .-Color[class*=-C53] { + color: #5F005F +} + +div.highlight .-Color[class*=-BGC53] { + background-color: #5F005F +} + +div.highlight .-Color[class*=-C54] { + color: #5F0087 +} + +div.highlight .-Color[class*=-BGC54] { + background-color: #5F0087 +} + +div.highlight .-Color[class*=-C55] { + color: #5F00AF +} + +div.highlight .-Color[class*=-BGC55] { + background-color: #5F00AF +} + +div.highlight .-Color[class*=-C56] { + color: #5F00D7 +} + +div.highlight .-Color[class*=-BGC56] { + background-color: #5F00D7 +} + +div.highlight .-Color[class*=-C57] { + color: #5F00FF +} + +div.highlight .-Color[class*=-BGC57] { + background-color: #5F00FF +} + +div.highlight .-Color[class*=-C58] { + color: #5F5F00 +} + +div.highlight .-Color[class*=-BGC58] { + background-color: #5F5F00 +} + +div.highlight .-Color[class*=-C59] { + color: #5F5F5F +} + +div.highlight .-Color[class*=-BGC59] { + background-color: #5F5F5F +} + +div.highlight .-Color[class*=-C60] { + color: #5F5F87 +} + +div.highlight .-Color[class*=-BGC60] { + background-color: #5F5F87 +} + +div.highlight .-Color[class*=-C61] { + color: #5F5FAF +} + +div.highlight .-Color[class*=-BGC61] { + background-color: #5F5FAF +} + +div.highlight .-Color[class*=-C62] { + color: #5F5FD7 +} + +div.highlight .-Color[class*=-BGC62] { + background-color: #5F5FD7 +} + +div.highlight .-Color[class*=-C63] { + color: #5F5FFF +} + +div.highlight .-Color[class*=-BGC63] { + background-color: #5F5FFF +} + +div.highlight .-Color[class*=-C64] { + color: #5F8700 +} + +div.highlight .-Color[class*=-BGC64] { + background-color: #5F8700 +} + +div.highlight .-Color[class*=-C65] { + color: #5F875F +} + +div.highlight .-Color[class*=-BGC65] { + background-color: #5F875F +} + +div.highlight .-Color[class*=-C66] { + color: #5F8787 +} + +div.highlight .-Color[class*=-BGC66] { + background-color: #5F8787 +} + +div.highlight .-Color[class*=-C67] { + color: #5F87AF +} + +div.highlight .-Color[class*=-BGC67] { + background-color: #5F87AF +} + +div.highlight .-Color[class*=-C68] { + color: #5F87D7 +} + +div.highlight .-Color[class*=-BGC68] { + background-color: #5F87D7 +} + +div.highlight .-Color[class*=-C69] { + color: #5F87FF +} + +div.highlight .-Color[class*=-BGC69] { + background-color: #5F87FF +} + +div.highlight .-Color[class*=-C70] { + color: #5FAF00 +} + +div.highlight .-Color[class*=-BGC70] { + background-color: #5FAF00 +} + +div.highlight .-Color[class*=-C71] { + color: #5FAF5F +} + +div.highlight .-Color[class*=-BGC71] { + background-color: #5FAF5F +} + +div.highlight .-Color[class*=-C72] { + color: #5FAF87 +} + +div.highlight .-Color[class*=-BGC72] { + background-color: #5FAF87 +} + +div.highlight .-Color[class*=-C73] { + color: #5FAFAF +} + +div.highlight .-Color[class*=-BGC73] { + background-color: #5FAFAF +} + +div.highlight .-Color[class*=-C74] { + color: #5FAFD7 +} + +div.highlight .-Color[class*=-BGC74] { + background-color: #5FAFD7 +} + +div.highlight .-Color[class*=-C75] { + color: #5FAFFF +} + +div.highlight .-Color[class*=-BGC75] { + background-color: #5FAFFF +} + +div.highlight .-Color[class*=-C76] { + color: #5FD700 +} + +div.highlight .-Color[class*=-BGC76] { + background-color: #5FD700 +} + +div.highlight .-Color[class*=-C77] { + color: #5FD75F +} + +div.highlight .-Color[class*=-BGC77] { + background-color: #5FD75F +} + +div.highlight .-Color[class*=-C78] { + color: #5FD787 +} + +div.highlight .-Color[class*=-BGC78] { + background-color: #5FD787 +} + +div.highlight .-Color[class*=-C79] { + color: #5FD7AF +} + +div.highlight .-Color[class*=-BGC79] { + background-color: #5FD7AF +} + +div.highlight .-Color[class*=-C80] { + color: #5FD7D7 +} + +div.highlight .-Color[class*=-BGC80] { + background-color: #5FD7D7 +} + +div.highlight .-Color[class*=-C81] { + color: #5FD7FF +} + +div.highlight .-Color[class*=-BGC81] { + background-color: #5FD7FF +} + +div.highlight .-Color[class*=-C82] { + color: #5FFF00 +} + +div.highlight .-Color[class*=-BGC82] { + background-color: #5FFF00 +} + +div.highlight .-Color[class*=-C83] { + color: #5FFF5F +} + +div.highlight .-Color[class*=-BGC83] { + background-color: #5FFF5F +} + +div.highlight .-Color[class*=-C84] { + color: #5FFF87 +} + +div.highlight .-Color[class*=-BGC84] { + background-color: #5FFF87 +} + +div.highlight .-Color[class*=-C85] { + color: #5FFFAF +} + +div.highlight .-Color[class*=-BGC85] { + background-color: #5FFFAF +} + +div.highlight .-Color[class*=-C86] { + color: #5FFFD7 +} + +div.highlight .-Color[class*=-BGC86] { + background-color: #5FFFD7 +} + +div.highlight .-Color[class*=-C87] { + color: #5FFFFF +} + +div.highlight .-Color[class*=-BGC87] { + background-color: #5FFFFF +} + +div.highlight .-Color[class*=-C88] { + color: #870000 +} + +div.highlight .-Color[class*=-BGC88] { + background-color: #870000 +} + +div.highlight .-Color[class*=-C89] { + color: #87005F +} + +div.highlight .-Color[class*=-BGC89] { + background-color: #87005F +} + +div.highlight .-Color[class*=-C90] { + color: #870087 +} + +div.highlight .-Color[class*=-BGC90] { + background-color: #870087 +} + +div.highlight .-Color[class*=-C91] { + color: #8700AF +} + +div.highlight .-Color[class*=-BGC91] { + background-color: #8700AF +} + +div.highlight .-Color[class*=-C92] { + color: #8700D7 +} + +div.highlight .-Color[class*=-BGC92] { + background-color: #8700D7 +} + +div.highlight .-Color[class*=-C93] { + color: #8700FF +} + +div.highlight .-Color[class*=-BGC93] { + background-color: #8700FF +} + +div.highlight .-Color[class*=-C94] { + color: #875F00 +} + +div.highlight .-Color[class*=-BGC94] { + background-color: #875F00 +} + +div.highlight .-Color[class*=-C95] { + color: #875F5F +} + +div.highlight .-Color[class*=-BGC95] { + background-color: #875F5F +} + +div.highlight .-Color[class*=-C96] { + color: #875F87 +} + +div.highlight .-Color[class*=-BGC96] { + background-color: #875F87 +} + +div.highlight .-Color[class*=-C97] { + color: #875FAF +} + +div.highlight .-Color[class*=-BGC97] { + background-color: #875FAF +} + +div.highlight .-Color[class*=-C98] { + color: #875FD7 +} + +div.highlight .-Color[class*=-BGC98] { + background-color: #875FD7 +} + +div.highlight .-Color[class*=-C99] { + color: #875FFF +} + +div.highlight .-Color[class*=-BGC99] { + background-color: #875FFF +} + +div.highlight .-Color[class*=-C100] { + color: #878700 +} + +div.highlight .-Color[class*=-BGC100] { + background-color: #878700 +} + +div.highlight .-Color[class*=-C101] { + color: #87875F +} + +div.highlight .-Color[class*=-BGC101] { + background-color: #87875F +} + +div.highlight .-Color[class*=-C102] { + color: #878787 +} + +div.highlight .-Color[class*=-BGC102] { + background-color: #878787 +} + +div.highlight .-Color[class*=-C103] { + color: #8787AF +} + +div.highlight .-Color[class*=-BGC103] { + background-color: #8787AF +} + +div.highlight .-Color[class*=-C104] { + color: #8787D7 +} + +div.highlight .-Color[class*=-BGC104] { + background-color: #8787D7 +} + +div.highlight .-Color[class*=-C105] { + color: #8787FF +} + +div.highlight .-Color[class*=-BGC105] { + background-color: #8787FF +} + +div.highlight .-Color[class*=-C106] { + color: #87AF00 +} + +div.highlight .-Color[class*=-BGC106] { + background-color: #87AF00 +} + +div.highlight .-Color[class*=-C107] { + color: #87AF5F +} + +div.highlight .-Color[class*=-BGC107] { + background-color: #87AF5F +} + +div.highlight .-Color[class*=-C108] { + color: #87AF87 +} + +div.highlight .-Color[class*=-BGC108] { + background-color: #87AF87 +} + +div.highlight .-Color[class*=-C109] { + color: #87AFAF +} + +div.highlight .-Color[class*=-BGC109] { + background-color: #87AFAF +} + +div.highlight .-Color[class*=-C110] { + color: #87AFD7 +} + +div.highlight .-Color[class*=-BGC110] { + background-color: #87AFD7 +} + +div.highlight .-Color[class*=-C111] { + color: #87AFFF +} + +div.highlight .-Color[class*=-BGC111] { + background-color: #87AFFF +} + +div.highlight .-Color[class*=-C112] { + color: #87D700 +} + +div.highlight .-Color[class*=-BGC112] { + background-color: #87D700 +} + +div.highlight .-Color[class*=-C113] { + color: #87D75F +} + +div.highlight .-Color[class*=-BGC113] { + background-color: #87D75F +} + +div.highlight .-Color[class*=-C114] { + color: #87D787 +} + +div.highlight .-Color[class*=-BGC114] { + background-color: #87D787 +} + +div.highlight .-Color[class*=-C115] { + color: #87D7AF +} + +div.highlight .-Color[class*=-BGC115] { + background-color: #87D7AF +} + +div.highlight .-Color[class*=-C116] { + color: #87D7D7 +} + +div.highlight .-Color[class*=-BGC116] { + background-color: #87D7D7 +} + +div.highlight .-Color[class*=-C117] { + color: #87D7FF +} + +div.highlight .-Color[class*=-BGC117] { + background-color: #87D7FF +} + +div.highlight .-Color[class*=-C118] { + color: #87FF00 +} + +div.highlight .-Color[class*=-BGC118] { + background-color: #87FF00 +} + +div.highlight .-Color[class*=-C119] { + color: #87FF5F +} + +div.highlight .-Color[class*=-BGC119] { + background-color: #87FF5F +} + +div.highlight .-Color[class*=-C120] { + color: #87FF87 +} + +div.highlight .-Color[class*=-BGC120] { + background-color: #87FF87 +} + +div.highlight .-Color[class*=-C121] { + color: #87FFAF +} + +div.highlight .-Color[class*=-BGC121] { + background-color: #87FFAF +} + +div.highlight .-Color[class*=-C122] { + color: #87FFD7 +} + +div.highlight .-Color[class*=-BGC122] { + background-color: #87FFD7 +} + +div.highlight .-Color[class*=-C123] { + color: #87FFFF +} + +div.highlight .-Color[class*=-BGC123] { + background-color: #87FFFF +} + +div.highlight .-Color[class*=-C124] { + color: #AF0000 +} + +div.highlight .-Color[class*=-BGC124] { + background-color: #AF0000 +} + +div.highlight .-Color[class*=-C125] { + color: #AF005F +} + +div.highlight .-Color[class*=-BGC125] { + background-color: #AF005F +} + +div.highlight .-Color[class*=-C126] { + color: #AF0087 +} + +div.highlight .-Color[class*=-BGC126] { + background-color: #AF0087 +} + +div.highlight .-Color[class*=-C127] { + color: #AF00AF +} + +div.highlight .-Color[class*=-BGC127] { + background-color: #AF00AF +} + +div.highlight .-Color[class*=-C128] { + color: #AF00D7 +} + +div.highlight .-Color[class*=-BGC128] { + background-color: #AF00D7 +} + +div.highlight .-Color[class*=-C129] { + color: #AF00FF +} + +div.highlight .-Color[class*=-BGC129] { + background-color: #AF00FF +} + +div.highlight .-Color[class*=-C130] { + color: #AF5F00 +} + +div.highlight .-Color[class*=-BGC130] { + background-color: #AF5F00 +} + +div.highlight .-Color[class*=-C131] { + color: #AF5F5F +} + +div.highlight .-Color[class*=-BGC131] { + background-color: #AF5F5F +} + +div.highlight .-Color[class*=-C132] { + color: #AF5F87 +} + +div.highlight .-Color[class*=-BGC132] { + background-color: #AF5F87 +} + +div.highlight .-Color[class*=-C133] { + color: #AF5FAF +} + +div.highlight .-Color[class*=-BGC133] { + background-color: #AF5FAF +} + +div.highlight .-Color[class*=-C134] { + color: #AF5FD7 +} + +div.highlight .-Color[class*=-BGC134] { + background-color: #AF5FD7 +} + +div.highlight .-Color[class*=-C135] { + color: #AF5FFF +} + +div.highlight .-Color[class*=-BGC135] { + background-color: #AF5FFF +} + +div.highlight .-Color[class*=-C136] { + color: #AF8700 +} + +div.highlight .-Color[class*=-BGC136] { + background-color: #AF8700 +} + +div.highlight .-Color[class*=-C137] { + color: #AF875F +} + +div.highlight .-Color[class*=-BGC137] { + background-color: #AF875F +} + +div.highlight .-Color[class*=-C138] { + color: #AF8787 +} + +div.highlight .-Color[class*=-BGC138] { + background-color: #AF8787 +} + +div.highlight .-Color[class*=-C139] { + color: #AF87AF +} + +div.highlight .-Color[class*=-BGC139] { + background-color: #AF87AF +} + +div.highlight .-Color[class*=-C140] { + color: #AF87D7 +} + +div.highlight .-Color[class*=-BGC140] { + background-color: #AF87D7 +} + +div.highlight .-Color[class*=-C141] { + color: #AF87FF +} + +div.highlight .-Color[class*=-BGC141] { + background-color: #AF87FF +} + +div.highlight .-Color[class*=-C142] { + color: #AFAF00 +} + +div.highlight .-Color[class*=-BGC142] { + background-color: #AFAF00 +} + +div.highlight .-Color[class*=-C143] { + color: #AFAF5F +} + +div.highlight .-Color[class*=-BGC143] { + background-color: #AFAF5F +} + +div.highlight .-Color[class*=-C144] { + color: #AFAF87 +} + +div.highlight .-Color[class*=-BGC144] { + background-color: #AFAF87 +} + +div.highlight .-Color[class*=-C145] { + color: #AFAFAF +} + +div.highlight .-Color[class*=-BGC145] { + background-color: #AFAFAF +} + +div.highlight .-Color[class*=-C146] { + color: #AFAFD7 +} + +div.highlight .-Color[class*=-BGC146] { + background-color: #AFAFD7 +} + +div.highlight .-Color[class*=-C147] { + color: #AFAFFF +} + +div.highlight .-Color[class*=-BGC147] { + background-color: #AFAFFF +} + +div.highlight .-Color[class*=-C148] { + color: #AFD700 +} + +div.highlight .-Color[class*=-BGC148] { + background-color: #AFD700 +} + +div.highlight .-Color[class*=-C149] { + color: #AFD75F +} + +div.highlight .-Color[class*=-BGC149] { + background-color: #AFD75F +} + +div.highlight .-Color[class*=-C150] { + color: #AFD787 +} + +div.highlight .-Color[class*=-BGC150] { + background-color: #AFD787 +} + +div.highlight .-Color[class*=-C151] { + color: #AFD7AF +} + +div.highlight .-Color[class*=-BGC151] { + background-color: #AFD7AF +} + +div.highlight .-Color[class*=-C152] { + color: #AFD7D7 +} + +div.highlight .-Color[class*=-BGC152] { + background-color: #AFD7D7 +} + +div.highlight .-Color[class*=-C153] { + color: #AFD7FF +} + +div.highlight .-Color[class*=-BGC153] { + background-color: #AFD7FF +} + +div.highlight .-Color[class*=-C154] { + color: #AFFF00 +} + +div.highlight .-Color[class*=-BGC154] { + background-color: #AFFF00 +} + +div.highlight .-Color[class*=-C155] { + color: #AFFF5F +} + +div.highlight .-Color[class*=-BGC155] { + background-color: #AFFF5F +} + +div.highlight .-Color[class*=-C156] { + color: #AFFF87 +} + +div.highlight .-Color[class*=-BGC156] { + background-color: #AFFF87 +} + +div.highlight .-Color[class*=-C157] { + color: #AFFFAF +} + +div.highlight .-Color[class*=-BGC157] { + background-color: #AFFFAF +} + +div.highlight .-Color[class*=-C158] { + color: #AFFFD7 +} + +div.highlight .-Color[class*=-BGC158] { + background-color: #AFFFD7 +} + +div.highlight .-Color[class*=-C159] { + color: #AFFFFF +} + +div.highlight .-Color[class*=-BGC159] { + background-color: #AFFFFF +} + +div.highlight .-Color[class*=-C160] { + color: #D70000 +} + +div.highlight .-Color[class*=-BGC160] { + background-color: #D70000 +} + +div.highlight .-Color[class*=-C161] { + color: #D7005F +} + +div.highlight .-Color[class*=-BGC161] { + background-color: #D7005F +} + +div.highlight .-Color[class*=-C162] { + color: #D70087 +} + +div.highlight .-Color[class*=-BGC162] { + background-color: #D70087 +} + +div.highlight .-Color[class*=-C163] { + color: #D700AF +} + +div.highlight .-Color[class*=-BGC163] { + background-color: #D700AF +} + +div.highlight .-Color[class*=-C164] { + color: #D700D7 +} + +div.highlight .-Color[class*=-BGC164] { + background-color: #D700D7 +} + +div.highlight .-Color[class*=-C165] { + color: #D700FF +} + +div.highlight .-Color[class*=-BGC165] { + background-color: #D700FF +} + +div.highlight .-Color[class*=-C166] { + color: #D75F00 +} + +div.highlight .-Color[class*=-BGC166] { + background-color: #D75F00 +} + +div.highlight .-Color[class*=-C167] { + color: #D75F5F +} + +div.highlight .-Color[class*=-BGC167] { + background-color: #D75F5F +} + +div.highlight .-Color[class*=-C168] { + color: #D75F87 +} + +div.highlight .-Color[class*=-BGC168] { + background-color: #D75F87 +} + +div.highlight .-Color[class*=-C169] { + color: #D75FAF +} + +div.highlight .-Color[class*=-BGC169] { + background-color: #D75FAF +} + +div.highlight .-Color[class*=-C170] { + color: #D75FD7 +} + +div.highlight .-Color[class*=-BGC170] { + background-color: #D75FD7 +} + +div.highlight .-Color[class*=-C171] { + color: #D75FFF +} + +div.highlight .-Color[class*=-BGC171] { + background-color: #D75FFF +} + +div.highlight .-Color[class*=-C172] { + color: #D78700 +} + +div.highlight .-Color[class*=-BGC172] { + background-color: #D78700 +} + +div.highlight .-Color[class*=-C173] { + color: #D7875F +} + +div.highlight .-Color[class*=-BGC173] { + background-color: #D7875F +} + +div.highlight .-Color[class*=-C174] { + color: #D78787 +} + +div.highlight .-Color[class*=-BGC174] { + background-color: #D78787 +} + +div.highlight .-Color[class*=-C175] { + color: #D787AF +} + +div.highlight .-Color[class*=-BGC175] { + background-color: #D787AF +} + +div.highlight .-Color[class*=-C176] { + color: #D787D7 +} + +div.highlight .-Color[class*=-BGC176] { + background-color: #D787D7 +} + +div.highlight .-Color[class*=-C177] { + color: #D787FF +} + +div.highlight .-Color[class*=-BGC177] { + background-color: #D787FF +} + +div.highlight .-Color[class*=-C178] { + color: #D7AF00 +} + +div.highlight .-Color[class*=-BGC178] { + background-color: #D7AF00 +} + +div.highlight .-Color[class*=-C179] { + color: #D7AF5F +} + +div.highlight .-Color[class*=-BGC179] { + background-color: #D7AF5F +} + +div.highlight .-Color[class*=-C180] { + color: #D7AF87 +} + +div.highlight .-Color[class*=-BGC180] { + background-color: #D7AF87 +} + +div.highlight .-Color[class*=-C181] { + color: #D7AFAF +} + +div.highlight .-Color[class*=-BGC181] { + background-color: #D7AFAF +} + +div.highlight .-Color[class*=-C182] { + color: #D7AFD7 +} + +div.highlight .-Color[class*=-BGC182] { + background-color: #D7AFD7 +} + +div.highlight .-Color[class*=-C183] { + color: #D7AFFF +} + +div.highlight .-Color[class*=-BGC183] { + background-color: #D7AFFF +} + +div.highlight .-Color[class*=-C184] { + color: #D7D700 +} + +div.highlight .-Color[class*=-BGC184] { + background-color: #D7D700 +} + +div.highlight .-Color[class*=-C185] { + color: #D7D75F +} + +div.highlight .-Color[class*=-BGC185] { + background-color: #D7D75F +} + +div.highlight .-Color[class*=-C186] { + color: #D7D787 +} + +div.highlight .-Color[class*=-BGC186] { + background-color: #D7D787 +} + +div.highlight .-Color[class*=-C187] { + color: #D7D7AF +} + +div.highlight .-Color[class*=-BGC187] { + background-color: #D7D7AF +} + +div.highlight .-Color[class*=-C188] { + color: #D7D7D7 +} + +div.highlight .-Color[class*=-BGC188] { + background-color: #D7D7D7 +} + +div.highlight .-Color[class*=-C189] { + color: #D7D7FF +} + +div.highlight .-Color[class*=-BGC189] { + background-color: #D7D7FF +} + +div.highlight .-Color[class*=-C190] { + color: #D7FF00 +} + +div.highlight .-Color[class*=-BGC190] { + background-color: #D7FF00 +} + +div.highlight .-Color[class*=-C191] { + color: #D7FF5F +} + +div.highlight .-Color[class*=-BGC191] { + background-color: #D7FF5F +} + +div.highlight .-Color[class*=-C192] { + color: #D7FF87 +} + +div.highlight .-Color[class*=-BGC192] { + background-color: #D7FF87 +} + +div.highlight .-Color[class*=-C193] { + color: #D7FFAF +} + +div.highlight .-Color[class*=-BGC193] { + background-color: #D7FFAF +} + +div.highlight .-Color[class*=-C194] { + color: #D7FFD7 +} + +div.highlight .-Color[class*=-BGC194] { + background-color: #D7FFD7 +} + +div.highlight .-Color[class*=-C195] { + color: #D7FFFF +} + +div.highlight .-Color[class*=-BGC195] { + background-color: #D7FFFF +} + +div.highlight .-Color[class*=-C196] { + color: #FF0000 +} + +div.highlight .-Color[class*=-BGC196] { + background-color: #FF0000 +} + +div.highlight .-Color[class*=-C197] { + color: #FF005F +} + +div.highlight .-Color[class*=-BGC197] { + background-color: #FF005F +} + +div.highlight .-Color[class*=-C198] { + color: #FF0087 +} + +div.highlight .-Color[class*=-BGC198] { + background-color: #FF0087 +} + +div.highlight .-Color[class*=-C199] { + color: #FF00AF +} + +div.highlight .-Color[class*=-BGC199] { + background-color: #FF00AF +} + +div.highlight .-Color[class*=-C200] { + color: #FF00D7 +} + +div.highlight .-Color[class*=-BGC200] { + background-color: #FF00D7 +} + +div.highlight .-Color[class*=-C201] { + color: #FF00FF +} + +div.highlight .-Color[class*=-BGC201] { + background-color: #FF00FF +} + +div.highlight .-Color[class*=-C202] { + color: #FF5F00 +} + +div.highlight .-Color[class*=-BGC202] { + background-color: #FF5F00 +} + +div.highlight .-Color[class*=-C203] { + color: #FF5F5F +} + +div.highlight .-Color[class*=-BGC203] { + background-color: #FF5F5F +} + +div.highlight .-Color[class*=-C204] { + color: #FF5F87 +} + +div.highlight .-Color[class*=-BGC204] { + background-color: #FF5F87 +} + +div.highlight .-Color[class*=-C205] { + color: #FF5FAF +} + +div.highlight .-Color[class*=-BGC205] { + background-color: #FF5FAF +} + +div.highlight .-Color[class*=-C206] { + color: #FF5FD7 +} + +div.highlight .-Color[class*=-BGC206] { + background-color: #FF5FD7 +} + +div.highlight .-Color[class*=-C207] { + color: #FF5FFF +} + +div.highlight .-Color[class*=-BGC207] { + background-color: #FF5FFF +} + +div.highlight .-Color[class*=-C208] { + color: #FF8700 +} + +div.highlight .-Color[class*=-BGC208] { + background-color: #FF8700 +} + +div.highlight .-Color[class*=-C209] { + color: #FF875F +} + +div.highlight .-Color[class*=-BGC209] { + background-color: #FF875F +} + +div.highlight .-Color[class*=-C210] { + color: #FF8787 +} + +div.highlight .-Color[class*=-BGC210] { + background-color: #FF8787 +} + +div.highlight .-Color[class*=-C211] { + color: #FF87AF +} + +div.highlight .-Color[class*=-BGC211] { + background-color: #FF87AF +} + +div.highlight .-Color[class*=-C212] { + color: #FF87D7 +} + +div.highlight .-Color[class*=-BGC212] { + background-color: #FF87D7 +} + +div.highlight .-Color[class*=-C213] { + color: #FF87FF +} + +div.highlight .-Color[class*=-BGC213] { + background-color: #FF87FF +} + +div.highlight .-Color[class*=-C214] { + color: #FFAF00 +} + +div.highlight .-Color[class*=-BGC214] { + background-color: #FFAF00 +} + +div.highlight .-Color[class*=-C215] { + color: #FFAF5F +} + +div.highlight .-Color[class*=-BGC215] { + background-color: #FFAF5F +} + +div.highlight .-Color[class*=-C216] { + color: #FFAF87 +} + +div.highlight .-Color[class*=-BGC216] { + background-color: #FFAF87 +} + +div.highlight .-Color[class*=-C217] { + color: #FFAFAF +} + +div.highlight .-Color[class*=-BGC217] { + background-color: #FFAFAF +} + +div.highlight .-Color[class*=-C218] { + color: #FFAFD7 +} + +div.highlight .-Color[class*=-BGC218] { + background-color: #FFAFD7 +} + +div.highlight .-Color[class*=-C219] { + color: #FFAFFF +} + +div.highlight .-Color[class*=-BGC219] { + background-color: #FFAFFF +} + +div.highlight .-Color[class*=-C220] { + color: #FFD700 +} + +div.highlight .-Color[class*=-BGC220] { + background-color: #FFD700 +} + +div.highlight .-Color[class*=-C221] { + color: #FFD75F +} + +div.highlight .-Color[class*=-BGC221] { + background-color: #FFD75F +} + +div.highlight .-Color[class*=-C222] { + color: #FFD787 +} + +div.highlight .-Color[class*=-BGC222] { + background-color: #FFD787 +} + +div.highlight .-Color[class*=-C223] { + color: #FFD7AF +} + +div.highlight .-Color[class*=-BGC223] { + background-color: #FFD7AF +} + +div.highlight .-Color[class*=-C224] { + color: #FFD7D7 +} + +div.highlight .-Color[class*=-BGC224] { + background-color: #FFD7D7 +} + +div.highlight .-Color[class*=-C225] { + color: #FFD7FF +} + +div.highlight .-Color[class*=-BGC225] { + background-color: #FFD7FF +} + +div.highlight .-Color[class*=-C226] { + color: #FFFF00 +} + +div.highlight .-Color[class*=-BGC226] { + background-color: #FFFF00 +} + +div.highlight .-Color[class*=-C227] { + color: #FFFF5F +} + +div.highlight .-Color[class*=-BGC227] { + background-color: #FFFF5F +} + +div.highlight .-Color[class*=-C228] { + color: #FFFF87 +} + +div.highlight .-Color[class*=-BGC228] { + background-color: #FFFF87 +} + +div.highlight .-Color[class*=-C229] { + color: #FFFFAF +} + +div.highlight .-Color[class*=-BGC229] { + background-color: #FFFFAF +} + +div.highlight .-Color[class*=-C230] { + color: #FFFFD7 +} + +div.highlight .-Color[class*=-BGC230] { + background-color: #FFFFD7 +} + +div.highlight .-Color[class*=-C231] { + color: #FFFFFF +} + +div.highlight .-Color[class*=-BGC231] { + background-color: #FFFFFF +} + +div.highlight .-Color[class*=-C232] { + color: #080808 +} + +div.highlight .-Color[class*=-BGC232] { + background-color: #080808 +} + +div.highlight .-Color[class*=-C233] { + color: #121212 +} + +div.highlight .-Color[class*=-BGC233] { + background-color: #121212 +} + +div.highlight .-Color[class*=-C234] { + color: #1C1C1C +} + +div.highlight .-Color[class*=-BGC234] { + background-color: #1C1C1C +} + +div.highlight .-Color[class*=-C235] { + color: #262626 +} + +div.highlight .-Color[class*=-BGC235] { + background-color: #262626 +} + +div.highlight .-Color[class*=-C236] { + color: #303030 +} + +div.highlight .-Color[class*=-BGC236] { + background-color: #303030 +} + +div.highlight .-Color[class*=-C237] { + color: #3A3A3A +} + +div.highlight .-Color[class*=-BGC237] { + background-color: #3A3A3A +} + +div.highlight .-Color[class*=-C238] { + color: #444444 +} + +div.highlight .-Color[class*=-BGC238] { + background-color: #444444 +} + +div.highlight .-Color[class*=-C239] { + color: #4E4E4E +} + +div.highlight .-Color[class*=-BGC239] { + background-color: #4E4E4E +} + +div.highlight .-Color[class*=-C240] { + color: #585858 +} + +div.highlight .-Color[class*=-BGC240] { + background-color: #585858 +} + +div.highlight .-Color[class*=-C241] { + color: #626262 +} + +div.highlight .-Color[class*=-BGC241] { + background-color: #626262 +} + +div.highlight .-Color[class*=-C242] { + color: #6C6C6C +} + +div.highlight .-Color[class*=-BGC242] { + background-color: #6C6C6C +} + +div.highlight .-Color[class*=-C243] { + color: #767676 +} + +div.highlight .-Color[class*=-BGC243] { + background-color: #767676 +} + +div.highlight .-Color[class*=-C244] { + color: #808080 +} + +div.highlight .-Color[class*=-BGC244] { + background-color: #808080 +} + +div.highlight .-Color[class*=-C245] { + color: #8A8A8A +} + +div.highlight .-Color[class*=-BGC245] { + background-color: #8A8A8A +} + +div.highlight .-Color[class*=-C246] { + color: #949494 +} + +div.highlight .-Color[class*=-BGC246] { + background-color: #949494 +} + +div.highlight .-Color[class*=-C247] { + color: #9E9E9E +} + +div.highlight .-Color[class*=-BGC247] { + background-color: #9E9E9E +} + +div.highlight .-Color[class*=-C248] { + color: #A8A8A8 +} + +div.highlight .-Color[class*=-BGC248] { + background-color: #A8A8A8 +} + +div.highlight .-Color[class*=-C249] { + color: #B2B2B2 +} + +div.highlight .-Color[class*=-BGC249] { + background-color: #B2B2B2 +} + +div.highlight .-Color[class*=-C250] { + color: #BCBCBC +} + +div.highlight .-Color[class*=-BGC250] { + background-color: #BCBCBC +} + +div.highlight .-Color[class*=-C251] { + color: #C6C6C6 +} + +div.highlight .-Color[class*=-BGC251] { + background-color: #C6C6C6 +} + +div.highlight .-Color[class*=-C252] { + color: #D0D0D0 +} + +div.highlight .-Color[class*=-BGC252] { + background-color: #D0D0D0 +} + +div.highlight .-Color[class*=-C253] { + color: #DADADA +} + +div.highlight .-Color[class*=-BGC253] { + background-color: #DADADA +} + +div.highlight .-Color[class*=-C254] { + color: #E4E4E4 +} + +div.highlight .-Color[class*=-BGC254] { + background-color: #E4E4E4 +} + +div.highlight .-Color[class*=-C255] { + color: #EEEEEE +} + +div.highlight .-Color[class*=-BGC255] { + background-color: #EEEEEE +} diff --git a/main/_static/play-solid.svg b/main/_static/play-solid.svg new file mode 100644 index 0000000..bcd81f7 --- /dev/null +++ b/main/_static/play-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/main/_static/plus.png b/main/_static/plus.png new file mode 100644 index 0000000..7107cec Binary files /dev/null and b/main/_static/plus.png differ diff --git a/main/_static/pygments.css b/main/_static/pygments.css new file mode 100644 index 0000000..997797f --- /dev/null +++ b/main/_static/pygments.css @@ -0,0 +1,152 @@ +html[data-theme="light"] .highlight pre { line-height: 125%; } +html[data-theme="light"] .highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight .hll { background-color: #7971292e } +html[data-theme="light"] .highlight { background: #fefefe; color: #545454 } +html[data-theme="light"] .highlight .c { color: #797129 } /* Comment */ +html[data-theme="light"] .highlight .err { color: #d91e18 } /* Error */ +html[data-theme="light"] .highlight .k { color: #7928a1 } /* Keyword */ +html[data-theme="light"] .highlight .l { color: #797129 } /* Literal */ +html[data-theme="light"] .highlight .n { color: #545454 } /* Name */ +html[data-theme="light"] .highlight .o { color: #008000 } /* Operator */ +html[data-theme="light"] .highlight .p { color: #545454 } /* Punctuation */ +html[data-theme="light"] .highlight .ch { color: #797129 } /* Comment.Hashbang */ +html[data-theme="light"] .highlight .cm { color: #797129 } /* Comment.Multiline */ +html[data-theme="light"] .highlight .cp { color: #797129 } /* Comment.Preproc */ +html[data-theme="light"] .highlight .cpf { color: #797129 } /* Comment.PreprocFile */ +html[data-theme="light"] .highlight .c1 { color: #797129 } /* Comment.Single */ +html[data-theme="light"] .highlight .cs { color: #797129 } /* Comment.Special */ +html[data-theme="light"] .highlight .gd { color: #007faa } /* Generic.Deleted */ +html[data-theme="light"] .highlight .ge { font-style: italic } /* Generic.Emph */ +html[data-theme="light"] .highlight .gh { color: #007faa } /* Generic.Heading */ +html[data-theme="light"] .highlight .gs { font-weight: bold } /* Generic.Strong */ +html[data-theme="light"] .highlight .gu { color: #007faa } /* Generic.Subheading */ +html[data-theme="light"] .highlight .kc { color: #7928a1 } /* Keyword.Constant */ +html[data-theme="light"] .highlight .kd { color: #7928a1 } /* Keyword.Declaration */ +html[data-theme="light"] .highlight .kn { color: #7928a1 } /* Keyword.Namespace */ +html[data-theme="light"] .highlight .kp { color: #7928a1 } /* Keyword.Pseudo */ +html[data-theme="light"] .highlight .kr { color: #7928a1 } /* Keyword.Reserved */ +html[data-theme="light"] .highlight .kt { color: #797129 } /* Keyword.Type */ +html[data-theme="light"] .highlight .ld { color: #797129 } /* Literal.Date */ +html[data-theme="light"] .highlight .m { color: #797129 } /* Literal.Number */ +html[data-theme="light"] .highlight .s { color: #008000 } /* Literal.String */ +html[data-theme="light"] .highlight .na { color: #797129 } /* Name.Attribute */ +html[data-theme="light"] .highlight .nb { color: #797129 } /* Name.Builtin */ +html[data-theme="light"] .highlight .nc { color: #007faa } /* Name.Class */ +html[data-theme="light"] .highlight .no { color: #007faa } /* Name.Constant */ +html[data-theme="light"] .highlight .nd { color: #797129 } /* Name.Decorator */ +html[data-theme="light"] .highlight .ni { color: #008000 } /* Name.Entity */ +html[data-theme="light"] .highlight .ne { color: #7928a1 } /* Name.Exception */ +html[data-theme="light"] .highlight .nf { color: #007faa } /* Name.Function */ +html[data-theme="light"] .highlight .nl { color: #797129 } /* Name.Label */ +html[data-theme="light"] .highlight .nn { color: #545454 } /* Name.Namespace */ +html[data-theme="light"] .highlight .nx { color: #545454 } /* Name.Other */ +html[data-theme="light"] .highlight .py { color: #007faa } /* Name.Property */ +html[data-theme="light"] .highlight .nt { color: #007faa } /* Name.Tag */ +html[data-theme="light"] .highlight .nv { color: #d91e18 } /* Name.Variable */ +html[data-theme="light"] .highlight .ow { color: #7928a1 } /* Operator.Word */ +html[data-theme="light"] .highlight .pm { color: #545454 } /* Punctuation.Marker */ +html[data-theme="light"] .highlight .w { color: #545454 } /* Text.Whitespace */ +html[data-theme="light"] .highlight .mb { color: #797129 } /* Literal.Number.Bin */ +html[data-theme="light"] .highlight .mf { color: #797129 } /* Literal.Number.Float */ +html[data-theme="light"] .highlight .mh { color: #797129 } /* Literal.Number.Hex */ +html[data-theme="light"] .highlight .mi { color: #797129 } /* Literal.Number.Integer */ +html[data-theme="light"] .highlight .mo { color: #797129 } /* Literal.Number.Oct */ +html[data-theme="light"] .highlight .sa { color: #008000 } /* Literal.String.Affix */ +html[data-theme="light"] .highlight .sb { color: #008000 } /* Literal.String.Backtick */ +html[data-theme="light"] .highlight .sc { color: #008000 } /* Literal.String.Char */ +html[data-theme="light"] .highlight .dl { color: #008000 } /* Literal.String.Delimiter */ +html[data-theme="light"] .highlight .sd { color: #008000 } /* Literal.String.Doc */ +html[data-theme="light"] .highlight .s2 { color: #008000 } /* Literal.String.Double */ +html[data-theme="light"] .highlight .se { color: #008000 } /* Literal.String.Escape */ +html[data-theme="light"] .highlight .sh { color: #008000 } /* Literal.String.Heredoc */ +html[data-theme="light"] .highlight .si { color: #008000 } /* Literal.String.Interpol */ +html[data-theme="light"] .highlight .sx { color: #008000 } /* Literal.String.Other */ +html[data-theme="light"] .highlight .sr { color: #d91e18 } /* Literal.String.Regex */ +html[data-theme="light"] .highlight .s1 { color: #008000 } /* Literal.String.Single */ +html[data-theme="light"] .highlight .ss { color: #007faa } /* Literal.String.Symbol */ +html[data-theme="light"] .highlight .bp { color: #797129 } /* Name.Builtin.Pseudo */ +html[data-theme="light"] .highlight .fm { color: #007faa } /* Name.Function.Magic */ +html[data-theme="light"] .highlight .vc { color: #d91e18 } /* Name.Variable.Class */ +html[data-theme="light"] .highlight .vg { color: #d91e18 } /* Name.Variable.Global */ +html[data-theme="light"] .highlight .vi { color: #d91e18 } /* Name.Variable.Instance */ +html[data-theme="light"] .highlight .vm { color: #797129 } /* Name.Variable.Magic */ +html[data-theme="light"] .highlight .il { color: #797129 } /* Literal.Number.Integer.Long */ +html[data-theme="dark"] .highlight pre { line-height: 125%; } +html[data-theme="dark"] .highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight .hll { background-color: #ffd9002e } +html[data-theme="dark"] .highlight { background: #2b2b2b; color: #f8f8f2 } +html[data-theme="dark"] .highlight .c { color: #ffd900 } /* Comment */ +html[data-theme="dark"] .highlight .err { color: #ffa07a } /* Error */ +html[data-theme="dark"] .highlight .k { color: #dcc6e0 } /* Keyword */ +html[data-theme="dark"] .highlight .l { color: #ffd900 } /* Literal */ +html[data-theme="dark"] .highlight .n { color: #f8f8f2 } /* Name */ +html[data-theme="dark"] .highlight .o { color: #abe338 } /* Operator */ +html[data-theme="dark"] .highlight .p { color: #f8f8f2 } /* Punctuation */ +html[data-theme="dark"] .highlight .ch { color: #ffd900 } /* Comment.Hashbang */ +html[data-theme="dark"] .highlight .cm { color: #ffd900 } /* Comment.Multiline */ +html[data-theme="dark"] .highlight .cp { color: #ffd900 } /* Comment.Preproc */ +html[data-theme="dark"] .highlight .cpf { color: #ffd900 } /* Comment.PreprocFile */ +html[data-theme="dark"] .highlight .c1 { color: #ffd900 } /* Comment.Single */ +html[data-theme="dark"] .highlight .cs { color: #ffd900 } /* Comment.Special */ +html[data-theme="dark"] .highlight .gd { color: #00e0e0 } /* Generic.Deleted */ +html[data-theme="dark"] .highlight .ge { font-style: italic } /* Generic.Emph */ +html[data-theme="dark"] .highlight .gh { color: #00e0e0 } /* Generic.Heading */ +html[data-theme="dark"] .highlight .gs { font-weight: bold } /* Generic.Strong */ +html[data-theme="dark"] .highlight .gu { color: #00e0e0 } /* Generic.Subheading */ +html[data-theme="dark"] .highlight .kc { color: #dcc6e0 } /* Keyword.Constant */ +html[data-theme="dark"] .highlight .kd { color: #dcc6e0 } /* Keyword.Declaration */ +html[data-theme="dark"] .highlight .kn { color: #dcc6e0 } /* Keyword.Namespace */ +html[data-theme="dark"] .highlight .kp { color: #dcc6e0 } /* Keyword.Pseudo */ +html[data-theme="dark"] .highlight .kr { color: #dcc6e0 } /* Keyword.Reserved */ +html[data-theme="dark"] .highlight .kt { color: #ffd900 } /* Keyword.Type */ +html[data-theme="dark"] .highlight .ld { color: #ffd900 } /* Literal.Date */ +html[data-theme="dark"] .highlight .m { color: #ffd900 } /* Literal.Number */ +html[data-theme="dark"] .highlight .s { color: #abe338 } /* Literal.String */ +html[data-theme="dark"] .highlight .na { color: #ffd900 } /* Name.Attribute */ +html[data-theme="dark"] .highlight .nb { color: #ffd900 } /* Name.Builtin */ +html[data-theme="dark"] .highlight .nc { color: #00e0e0 } /* Name.Class */ +html[data-theme="dark"] .highlight .no { color: #00e0e0 } /* Name.Constant */ +html[data-theme="dark"] .highlight .nd { color: #ffd900 } /* Name.Decorator */ +html[data-theme="dark"] .highlight .ni { color: #abe338 } /* Name.Entity */ +html[data-theme="dark"] .highlight .ne { color: #dcc6e0 } /* Name.Exception */ +html[data-theme="dark"] .highlight .nf { color: #00e0e0 } /* Name.Function */ +html[data-theme="dark"] .highlight .nl { color: #ffd900 } /* Name.Label */ +html[data-theme="dark"] .highlight .nn { color: #f8f8f2 } /* Name.Namespace */ +html[data-theme="dark"] .highlight .nx { color: #f8f8f2 } /* Name.Other */ +html[data-theme="dark"] .highlight .py { color: #00e0e0 } /* Name.Property */ +html[data-theme="dark"] .highlight .nt { color: #00e0e0 } /* Name.Tag */ +html[data-theme="dark"] .highlight .nv { color: #ffa07a } /* Name.Variable */ +html[data-theme="dark"] .highlight .ow { color: #dcc6e0 } /* Operator.Word */ +html[data-theme="dark"] .highlight .pm { color: #f8f8f2 } /* Punctuation.Marker */ +html[data-theme="dark"] .highlight .w { color: #f8f8f2 } /* Text.Whitespace */ +html[data-theme="dark"] .highlight .mb { color: #ffd900 } /* Literal.Number.Bin */ +html[data-theme="dark"] .highlight .mf { color: #ffd900 } /* Literal.Number.Float */ +html[data-theme="dark"] .highlight .mh { color: #ffd900 } /* Literal.Number.Hex */ +html[data-theme="dark"] .highlight .mi { color: #ffd900 } /* Literal.Number.Integer */ +html[data-theme="dark"] .highlight .mo { color: #ffd900 } /* Literal.Number.Oct */ +html[data-theme="dark"] .highlight .sa { color: #abe338 } /* Literal.String.Affix */ +html[data-theme="dark"] .highlight .sb { color: #abe338 } /* Literal.String.Backtick */ +html[data-theme="dark"] .highlight .sc { color: #abe338 } /* Literal.String.Char */ +html[data-theme="dark"] .highlight .dl { color: #abe338 } /* Literal.String.Delimiter */ +html[data-theme="dark"] .highlight .sd { color: #abe338 } /* Literal.String.Doc */ +html[data-theme="dark"] .highlight .s2 { color: #abe338 } /* Literal.String.Double */ +html[data-theme="dark"] .highlight .se { color: #abe338 } /* Literal.String.Escape */ +html[data-theme="dark"] .highlight .sh { color: #abe338 } /* Literal.String.Heredoc */ +html[data-theme="dark"] .highlight .si { color: #abe338 } /* Literal.String.Interpol */ +html[data-theme="dark"] .highlight .sx { color: #abe338 } /* Literal.String.Other */ +html[data-theme="dark"] .highlight .sr { color: #ffa07a } /* Literal.String.Regex */ +html[data-theme="dark"] .highlight .s1 { color: #abe338 } /* Literal.String.Single */ +html[data-theme="dark"] .highlight .ss { color: #00e0e0 } /* Literal.String.Symbol */ +html[data-theme="dark"] .highlight .bp { color: #ffd900 } /* Name.Builtin.Pseudo */ +html[data-theme="dark"] .highlight .fm { color: #00e0e0 } /* Name.Function.Magic */ +html[data-theme="dark"] .highlight .vc { color: #ffa07a } /* Name.Variable.Class */ +html[data-theme="dark"] .highlight .vg { color: #ffa07a } /* Name.Variable.Global */ +html[data-theme="dark"] .highlight .vi { color: #ffa07a } /* Name.Variable.Instance */ +html[data-theme="dark"] .highlight .vm { color: #ffd900 } /* Name.Variable.Magic */ +html[data-theme="dark"] .highlight .il { color: #ffd900 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/main/_static/sbt-webpack-macros.html b/main/_static/sbt-webpack-macros.html new file mode 100644 index 0000000..6cbf559 --- /dev/null +++ b/main/_static/sbt-webpack-macros.html @@ -0,0 +1,11 @@ + +{% macro head_pre_bootstrap() %} + +{% endmacro %} + +{% macro body_post() %} + +{% endmacro %} diff --git a/main/_static/scripts/bootstrap.js b/main/_static/scripts/bootstrap.js new file mode 100644 index 0000000..4e209b0 --- /dev/null +++ b/main/_static/scripts/bootstrap.js @@ -0,0 +1,3 @@ +/*! For license information please see bootstrap.js.LICENSE.txt */ +(()=>{"use strict";var t={d:(e,i)=>{for(var n in i)t.o(i,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:i[n]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};t.r(e),t.d(e,{afterMain:()=>E,afterRead:()=>v,afterWrite:()=>C,applyStyles:()=>$,arrow:()=>J,auto:()=>a,basePlacements:()=>l,beforeMain:()=>y,beforeRead:()=>_,beforeWrite:()=>A,bottom:()=>s,clippingParents:()=>d,computeStyles:()=>it,createPopper:()=>Dt,createPopperBase:()=>St,createPopperLite:()=>$t,detectOverflow:()=>_t,end:()=>h,eventListeners:()=>st,flip:()=>bt,hide:()=>wt,left:()=>r,main:()=>w,modifierPhases:()=>O,offset:()=>Et,placements:()=>g,popper:()=>f,popperGenerator:()=>Lt,popperOffsets:()=>At,preventOverflow:()=>Tt,read:()=>b,reference:()=>p,right:()=>o,start:()=>c,top:()=>n,variationPlacements:()=>m,viewport:()=>u,write:()=>T});var i={};t.r(i),t.d(i,{Alert:()=>Oe,Button:()=>ke,Carousel:()=>ri,Collapse:()=>yi,Dropdown:()=>Vi,Modal:()=>xn,Offcanvas:()=>Vn,Popover:()=>fs,ScrollSpy:()=>Ts,Tab:()=>Ks,Toast:()=>lo,Tooltip:()=>hs});var n="top",s="bottom",o="right",r="left",a="auto",l=[n,s,o,r],c="start",h="end",d="clippingParents",u="viewport",f="popper",p="reference",m=l.reduce((function(t,e){return t.concat([e+"-"+c,e+"-"+h])}),[]),g=[].concat(l,[a]).reduce((function(t,e){return t.concat([e,e+"-"+c,e+"-"+h])}),[]),_="beforeRead",b="read",v="afterRead",y="beforeMain",w="main",E="afterMain",A="beforeWrite",T="write",C="afterWrite",O=[_,b,v,y,w,E,A,T,C];function x(t){return t?(t.nodeName||"").toLowerCase():null}function k(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function L(t){return t instanceof k(t).Element||t instanceof Element}function S(t){return t instanceof k(t).HTMLElement||t instanceof HTMLElement}function D(t){return"undefined"!=typeof ShadowRoot&&(t instanceof k(t).ShadowRoot||t instanceof ShadowRoot)}const $={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];S(s)&&x(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});S(n)&&x(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function I(t){return t.split("-")[0]}var N=Math.max,P=Math.min,M=Math.round;function j(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function F(){return!/^((?!chrome|android).)*safari/i.test(j())}function H(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&S(t)&&(s=t.offsetWidth>0&&M(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&M(n.height)/t.offsetHeight||1);var r=(L(t)?k(t):window).visualViewport,a=!F()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function B(t){var e=H(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function W(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&D(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function z(t){return k(t).getComputedStyle(t)}function R(t){return["table","td","th"].indexOf(x(t))>=0}function q(t){return((L(t)?t.ownerDocument:t.document)||window.document).documentElement}function V(t){return"html"===x(t)?t:t.assignedSlot||t.parentNode||(D(t)?t.host:null)||q(t)}function Y(t){return S(t)&&"fixed"!==z(t).position?t.offsetParent:null}function K(t){for(var e=k(t),i=Y(t);i&&R(i)&&"static"===z(i).position;)i=Y(i);return i&&("html"===x(i)||"body"===x(i)&&"static"===z(i).position)?e:i||function(t){var e=/firefox/i.test(j());if(/Trident/i.test(j())&&S(t)&&"fixed"===z(t).position)return null;var i=V(t);for(D(i)&&(i=i.host);S(i)&&["html","body"].indexOf(x(i))<0;){var n=z(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Q(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function X(t,e,i){return N(t,P(e,i))}function U(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function G(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const J={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,a=t.name,c=t.options,h=i.elements.arrow,d=i.modifiersData.popperOffsets,u=I(i.placement),f=Q(u),p=[r,o].indexOf(u)>=0?"height":"width";if(h&&d){var m=function(t,e){return U("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:G(t,l))}(c.padding,i),g=B(h),_="y"===f?n:r,b="y"===f?s:o,v=i.rects.reference[p]+i.rects.reference[f]-d[f]-i.rects.popper[p],y=d[f]-i.rects.reference[f],w=K(h),E=w?"y"===f?w.clientHeight||0:w.clientWidth||0:0,A=v/2-y/2,T=m[_],C=E-g[p]-m[b],O=E/2-g[p]/2+A,x=X(T,O,C),k=f;i.modifiersData[a]=((e={})[k]=x,e.centerOffset=x-O,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&W(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Z(t){return t.split("-")[1]}var tt={top:"auto",right:"auto",bottom:"auto",left:"auto"};function et(t){var e,i=t.popper,a=t.popperRect,l=t.placement,c=t.variation,d=t.offsets,u=t.position,f=t.gpuAcceleration,p=t.adaptive,m=t.roundOffsets,g=t.isFixed,_=d.x,b=void 0===_?0:_,v=d.y,y=void 0===v?0:v,w="function"==typeof m?m({x:b,y}):{x:b,y};b=w.x,y=w.y;var E=d.hasOwnProperty("x"),A=d.hasOwnProperty("y"),T=r,C=n,O=window;if(p){var x=K(i),L="clientHeight",S="clientWidth";x===k(i)&&"static"!==z(x=q(i)).position&&"absolute"===u&&(L="scrollHeight",S="scrollWidth"),(l===n||(l===r||l===o)&&c===h)&&(C=s,y-=(g&&x===O&&O.visualViewport?O.visualViewport.height:x[L])-a.height,y*=f?1:-1),l!==r&&(l!==n&&l!==s||c!==h)||(T=o,b-=(g&&x===O&&O.visualViewport?O.visualViewport.width:x[S])-a.width,b*=f?1:-1)}var D,$=Object.assign({position:u},p&&tt),I=!0===m?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:M(i*s)/s||0,y:M(n*s)/s||0}}({x:b,y},k(i)):{x:b,y};return b=I.x,y=I.y,f?Object.assign({},$,((D={})[C]=A?"0":"",D[T]=E?"0":"",D.transform=(O.devicePixelRatio||1)<=1?"translate("+b+"px, "+y+"px)":"translate3d("+b+"px, "+y+"px, 0)",D)):Object.assign({},$,((e={})[C]=A?y+"px":"",e[T]=E?b+"px":"",e.transform="",e))}const it={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:I(e.placement),variation:Z(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,et(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,et(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var nt={passive:!0};const st={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=k(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,nt)})),a&&l.addEventListener("resize",i.update,nt),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,nt)})),a&&l.removeEventListener("resize",i.update,nt)}},data:{}};var ot={left:"right",right:"left",bottom:"top",top:"bottom"};function rt(t){return t.replace(/left|right|bottom|top/g,(function(t){return ot[t]}))}var at={start:"end",end:"start"};function lt(t){return t.replace(/start|end/g,(function(t){return at[t]}))}function ct(t){var e=k(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function ht(t){return H(q(t)).left+ct(t).scrollLeft}function dt(t){var e=z(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function ut(t){return["html","body","#document"].indexOf(x(t))>=0?t.ownerDocument.body:S(t)&&dt(t)?t:ut(V(t))}function ft(t,e){var i;void 0===e&&(e=[]);var n=ut(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=k(n),r=s?[o].concat(o.visualViewport||[],dt(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(ft(V(r)))}function pt(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function mt(t,e,i){return e===u?pt(function(t,e){var i=k(t),n=q(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=F();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+ht(t),y:l}}(t,i)):L(e)?function(t,e){var i=H(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):pt(function(t){var e,i=q(t),n=ct(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=N(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=N(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+ht(t),l=-n.scrollTop;return"rtl"===z(s||i).direction&&(a+=N(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(q(t)))}function gt(t){var e,i=t.reference,a=t.element,l=t.placement,d=l?I(l):null,u=l?Z(l):null,f=i.x+i.width/2-a.width/2,p=i.y+i.height/2-a.height/2;switch(d){case n:e={x:f,y:i.y-a.height};break;case s:e={x:f,y:i.y+i.height};break;case o:e={x:i.x+i.width,y:p};break;case r:e={x:i.x-a.width,y:p};break;default:e={x:i.x,y:i.y}}var m=d?Q(d):null;if(null!=m){var g="y"===m?"height":"width";switch(u){case c:e[m]=e[m]-(i[g]/2-a[g]/2);break;case h:e[m]=e[m]+(i[g]/2-a[g]/2)}}return e}function _t(t,e){void 0===e&&(e={});var i=e,r=i.placement,a=void 0===r?t.placement:r,c=i.strategy,h=void 0===c?t.strategy:c,m=i.boundary,g=void 0===m?d:m,_=i.rootBoundary,b=void 0===_?u:_,v=i.elementContext,y=void 0===v?f:v,w=i.altBoundary,E=void 0!==w&&w,A=i.padding,T=void 0===A?0:A,C=U("number"!=typeof T?T:G(T,l)),O=y===f?p:f,k=t.rects.popper,D=t.elements[E?O:y],$=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=ft(V(t)),i=["absolute","fixed"].indexOf(z(t).position)>=0&&S(t)?K(t):t;return L(i)?e.filter((function(t){return L(t)&&W(t,i)&&"body"!==x(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=mt(t,i,n);return e.top=N(s.top,e.top),e.right=P(s.right,e.right),e.bottom=P(s.bottom,e.bottom),e.left=N(s.left,e.left),e}),mt(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(L(D)?D:D.contextElement||q(t.elements.popper),g,b,h),I=H(t.elements.reference),M=gt({reference:I,element:k,strategy:"absolute",placement:a}),j=pt(Object.assign({},k,M)),F=y===f?j:I,B={top:$.top-F.top+C.top,bottom:F.bottom-$.bottom+C.bottom,left:$.left-F.left+C.left,right:F.right-$.right+C.right},R=t.modifiersData.offset;if(y===f&&R){var Y=R[a];Object.keys(B).forEach((function(t){var e=[o,s].indexOf(t)>=0?1:-1,i=[n,s].indexOf(t)>=0?"y":"x";B[t]+=Y[i]*e}))}return B}const bt={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,h=t.name;if(!e.modifiersData[h]._skip){for(var d=i.mainAxis,u=void 0===d||d,f=i.altAxis,p=void 0===f||f,_=i.fallbackPlacements,b=i.padding,v=i.boundary,y=i.rootBoundary,w=i.altBoundary,E=i.flipVariations,A=void 0===E||E,T=i.allowedAutoPlacements,C=e.options.placement,O=I(C),x=_||(O!==C&&A?function(t){if(I(t)===a)return[];var e=rt(t);return[lt(t),e,lt(e)]}(C):[rt(C)]),k=[C].concat(x).reduce((function(t,i){return t.concat(I(i)===a?function(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,c=i.allowedAutoPlacements,h=void 0===c?g:c,d=Z(n),u=d?a?m:m.filter((function(t){return Z(t)===d})):l,f=u.filter((function(t){return h.indexOf(t)>=0}));0===f.length&&(f=u);var p=f.reduce((function(e,i){return e[i]=_t(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[I(i)],e}),{});return Object.keys(p).sort((function(t,e){return p[t]-p[e]}))}(e,{placement:i,boundary:v,rootBoundary:y,padding:b,flipVariations:A,allowedAutoPlacements:T}):i)}),[]),L=e.rects.reference,S=e.rects.popper,D=new Map,$=!0,N=k[0],P=0;P=0,B=H?"width":"height",W=_t(e,{placement:M,boundary:v,rootBoundary:y,altBoundary:w,padding:b}),z=H?F?o:r:F?s:n;L[B]>S[B]&&(z=rt(z));var R=rt(z),q=[];if(u&&q.push(W[j]<=0),p&&q.push(W[z]<=0,W[R]<=0),q.every((function(t){return t}))){N=M,$=!1;break}D.set(M,q)}if($)for(var V=function(t){var e=k.find((function(e){var i=D.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return N=e,"break"},Y=A?3:1;Y>0&&"break"!==V(Y);Y--);e.placement!==N&&(e.modifiersData[h]._skip=!0,e.placement=N,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function vt(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function yt(t){return[n,o,s,r].some((function(e){return t[e]>=0}))}const wt={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=_t(e,{elementContext:"reference"}),a=_t(e,{altBoundary:!0}),l=vt(r,n),c=vt(a,s,o),h=yt(l),d=yt(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},Et={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,s=t.name,a=i.offset,l=void 0===a?[0,0]:a,c=g.reduce((function(t,i){return t[i]=function(t,e,i){var s=I(t),a=[r,n].indexOf(s)>=0?-1:1,l="function"==typeof i?i(Object.assign({},e,{placement:t})):i,c=l[0],h=l[1];return c=c||0,h=(h||0)*a,[r,o].indexOf(s)>=0?{x:h,y:c}:{x:c,y:h}}(i,e.rects,l),t}),{}),h=c[e.placement],d=h.x,u=h.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=d,e.modifiersData.popperOffsets.y+=u),e.modifiersData[s]=c}},At={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=gt({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},Tt={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,a=t.name,l=i.mainAxis,h=void 0===l||l,d=i.altAxis,u=void 0!==d&&d,f=i.boundary,p=i.rootBoundary,m=i.altBoundary,g=i.padding,_=i.tether,b=void 0===_||_,v=i.tetherOffset,y=void 0===v?0:v,w=_t(e,{boundary:f,rootBoundary:p,padding:g,altBoundary:m}),E=I(e.placement),A=Z(e.placement),T=!A,C=Q(E),O="x"===C?"y":"x",x=e.modifiersData.popperOffsets,k=e.rects.reference,L=e.rects.popper,S="function"==typeof y?y(Object.assign({},e.rects,{placement:e.placement})):y,D="number"==typeof S?{mainAxis:S,altAxis:S}:Object.assign({mainAxis:0,altAxis:0},S),$=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,M={x:0,y:0};if(x){if(h){var j,F="y"===C?n:r,H="y"===C?s:o,W="y"===C?"height":"width",z=x[C],R=z+w[F],q=z-w[H],V=b?-L[W]/2:0,Y=A===c?k[W]:L[W],U=A===c?-L[W]:-k[W],G=e.elements.arrow,J=b&&G?B(G):{width:0,height:0},tt=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},et=tt[F],it=tt[H],nt=X(0,k[W],J[W]),st=T?k[W]/2-V-nt-et-D.mainAxis:Y-nt-et-D.mainAxis,ot=T?-k[W]/2+V+nt+it+D.mainAxis:U+nt+it+D.mainAxis,rt=e.elements.arrow&&K(e.elements.arrow),at=rt?"y"===C?rt.clientTop||0:rt.clientLeft||0:0,lt=null!=(j=null==$?void 0:$[C])?j:0,ct=z+ot-lt,ht=X(b?P(R,z+st-lt-at):R,z,b?N(q,ct):q);x[C]=ht,M[C]=ht-z}if(u){var dt,ut="x"===C?n:r,ft="x"===C?s:o,pt=x[O],mt="y"===O?"height":"width",gt=pt+w[ut],bt=pt-w[ft],vt=-1!==[n,r].indexOf(E),yt=null!=(dt=null==$?void 0:$[O])?dt:0,wt=vt?gt:pt-k[mt]-L[mt]-yt+D.altAxis,Et=vt?pt+k[mt]+L[mt]-yt-D.altAxis:bt,At=b&&vt?function(t,e,i){var n=X(t,e,i);return n>i?i:n}(wt,pt,Et):X(b?wt:gt,pt,b?Et:bt);x[O]=At,M[O]=At-pt}e.modifiersData[a]=M}},requiresIfExists:["offset"]};function Ct(t,e,i){void 0===i&&(i=!1);var n,s,o=S(e),r=S(e)&&function(t){var e=t.getBoundingClientRect(),i=M(e.width)/t.offsetWidth||1,n=M(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=q(e),l=H(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==x(e)||dt(a))&&(c=(n=e)!==k(n)&&S(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:ct(n)),S(e)?((h=H(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=ht(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function Ot(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var xt={placement:"bottom",modifiers:[],strategy:"absolute"};function kt(){for(var t=arguments.length,e=new Array(t),i=0;iIt.has(t)&&It.get(t).get(e)||null,remove(t,e){if(!It.has(t))return;const i=It.get(t);i.delete(e),0===i.size&&It.delete(t)}},Pt="transitionend",Mt=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),jt=t=>{t.dispatchEvent(new Event(Pt))},Ft=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),Ht=t=>Ft(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(Mt(t)):null,Bt=t=>{if(!Ft(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},Wt=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),zt=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?zt(t.parentNode):null},Rt=()=>{},qt=t=>{t.offsetHeight},Vt=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,Yt=[],Kt=()=>"rtl"===document.documentElement.dir,Qt=t=>{var e;e=()=>{const e=Vt();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(Yt.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of Yt)t()})),Yt.push(e)):e()},Xt=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,Ut=(t,e,i=!0)=>{if(!i)return void Xt(t);const n=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let s=!1;const o=({target:i})=>{i===e&&(s=!0,e.removeEventListener(Pt,o),Xt(t))};e.addEventListener(Pt,o),setTimeout((()=>{s||jt(e)}),n)},Gt=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},Jt=/[^.]*(?=\..*)\.|.*/,Zt=/\..*/,te=/::\d+$/,ee={};let ie=1;const ne={mouseenter:"mouseover",mouseleave:"mouseout"},se=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function oe(t,e){return e&&`${e}::${ie++}`||t.uidEvent||ie++}function re(t){const e=oe(t);return t.uidEvent=e,ee[e]=ee[e]||{},ee[e]}function ae(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function le(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=ue(t);return se.has(o)||(o=t),[n,s,o]}function ce(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=le(e,i,n);if(e in ne){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=re(t),c=l[a]||(l[a]={}),h=ae(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=oe(r,e.replace(Jt,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return pe(s,{delegateTarget:r}),n.oneOff&&fe.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return pe(n,{delegateTarget:t}),i.oneOff&&fe.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function he(t,e,i,n,s){const o=ae(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function de(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&he(t,e,i,r.callable,r.delegationSelector)}function ue(t){return t=t.replace(Zt,""),ne[t]||t}const fe={on(t,e,i,n){ce(t,e,i,n,!1)},one(t,e,i,n){ce(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=le(e,i,n),a=r!==e,l=re(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))de(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(te,"");a&&!e.includes(s)||he(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;he(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=Vt();let s=null,o=!0,r=!0,a=!1;e!==ue(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=pe(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function pe(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function me(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function ge(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const _e={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${ge(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${ge(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=me(t.dataset[n])}return e},getDataAttribute:(t,e)=>me(t.getAttribute(`data-bs-${ge(e)}`))};class be{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=Ft(e)?_e.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...Ft(e)?_e.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[n,s]of Object.entries(e)){const e=t[n],o=Ft(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(o))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${o}" but expected type "${s}".`)}var i}}class ve extends be{constructor(t,e){super(),(t=Ht(t))&&(this._element=t,this._config=this._getConfig(e),Nt.set(this._element,this.constructor.DATA_KEY,this))}dispose(){Nt.remove(this._element,this.constructor.DATA_KEY),fe.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){Ut(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return Nt.get(Ht(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.2"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const ye=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?Mt(i.trim()):null}return e},we={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!Wt(t)&&Bt(t)))},getSelectorFromElement(t){const e=ye(t);return e&&we.findOne(e)?e:null},getElementFromSelector(t){const e=ye(t);return e?we.findOne(e):null},getMultipleElementsFromSelector(t){const e=ye(t);return e?we.find(e):[]}},Ee=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;fe.on(document,i,`[data-bs-dismiss="${n}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),Wt(this))return;const s=we.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()}))},Ae=".bs.alert",Te=`close${Ae}`,Ce=`closed${Ae}`;class Oe extends ve{static get NAME(){return"alert"}close(){if(fe.trigger(this._element,Te).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),fe.trigger(this._element,Ce),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=Oe.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}Ee(Oe,"close"),Qt(Oe);const xe='[data-bs-toggle="button"]';class ke extends ve{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=ke.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}fe.on(document,"click.bs.button.data-api",xe,(t=>{t.preventDefault();const e=t.target.closest(xe);ke.getOrCreateInstance(e).toggle()})),Qt(ke);const Le=".bs.swipe",Se=`touchstart${Le}`,De=`touchmove${Le}`,$e=`touchend${Le}`,Ie=`pointerdown${Le}`,Ne=`pointerup${Le}`,Pe={endCallback:null,leftCallback:null,rightCallback:null},Me={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class je extends be{constructor(t,e){super(),this._element=t,t&&je.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return Pe}static get DefaultType(){return Me}static get NAME(){return"swipe"}dispose(){fe.off(this._element,Le)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),Xt(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&Xt(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(fe.on(this._element,Ie,(t=>this._start(t))),fe.on(this._element,Ne,(t=>this._end(t))),this._element.classList.add("pointer-event")):(fe.on(this._element,Se,(t=>this._start(t))),fe.on(this._element,De,(t=>this._move(t))),fe.on(this._element,$e,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const Fe=".bs.carousel",He=".data-api",Be="next",We="prev",ze="left",Re="right",qe=`slide${Fe}`,Ve=`slid${Fe}`,Ye=`keydown${Fe}`,Ke=`mouseenter${Fe}`,Qe=`mouseleave${Fe}`,Xe=`dragstart${Fe}`,Ue=`load${Fe}${He}`,Ge=`click${Fe}${He}`,Je="carousel",Ze="active",ti=".active",ei=".carousel-item",ii=ti+ei,ni={ArrowLeft:Re,ArrowRight:ze},si={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},oi={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class ri extends ve{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=we.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===Je&&this.cycle()}static get Default(){return si}static get DefaultType(){return oi}static get NAME(){return"carousel"}next(){this._slide(Be)}nextWhenVisible(){!document.hidden&&Bt(this._element)&&this.next()}prev(){this._slide(We)}pause(){this._isSliding&&jt(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?fe.one(this._element,Ve,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void fe.one(this._element,Ve,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?Be:We;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&fe.on(this._element,Ye,(t=>this._keydown(t))),"hover"===this._config.pause&&(fe.on(this._element,Ke,(()=>this.pause())),fe.on(this._element,Qe,(()=>this._maybeEnableCycle()))),this._config.touch&&je.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of we.find(".carousel-item img",this._element))fe.on(t,Xe,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(ze)),rightCallback:()=>this._slide(this._directionToOrder(Re)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new je(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=ni[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=we.findOne(ti,this._indicatorsElement);e.classList.remove(Ze),e.removeAttribute("aria-current");const i=we.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(Ze),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===Be,s=e||Gt(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>fe.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(qe).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),qt(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(Ze),i.classList.remove(Ze,c,l),this._isSliding=!1,r(Ve)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return we.findOne(ii,this._element)}_getItems(){return we.find(ei,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return Kt()?t===ze?We:Be:t===ze?Be:We}_orderToDirection(t){return Kt()?t===We?ze:Re:t===We?Re:ze}static jQueryInterface(t){return this.each((function(){const e=ri.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}fe.on(document,Ge,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=we.getElementFromSelector(this);if(!e||!e.classList.contains(Je))return;t.preventDefault();const i=ri.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===_e.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),fe.on(window,Ue,(()=>{const t=we.find('[data-bs-ride="carousel"]');for(const e of t)ri.getOrCreateInstance(e)})),Qt(ri);const ai=".bs.collapse",li=`show${ai}`,ci=`shown${ai}`,hi=`hide${ai}`,di=`hidden${ai}`,ui=`click${ai}.data-api`,fi="show",pi="collapse",mi="collapsing",gi=`:scope .${pi} .${pi}`,_i='[data-bs-toggle="collapse"]',bi={parent:null,toggle:!0},vi={parent:"(null|element)",toggle:"boolean"};class yi extends ve{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=we.find(_i);for(const t of i){const e=we.getSelectorFromElement(t),i=we.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return bi}static get DefaultType(){return vi}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>yi.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(fe.trigger(this._element,li).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(pi),this._element.classList.add(mi),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(mi),this._element.classList.add(pi,fi),this._element.style[e]="",fe.trigger(this._element,ci)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(fe.trigger(this._element,hi).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,qt(this._element),this._element.classList.add(mi),this._element.classList.remove(pi,fi);for(const t of this._triggerArray){const e=we.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(mi),this._element.classList.add(pi),fe.trigger(this._element,di)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(fi)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=Ht(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(_i);for(const e of t){const t=we.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=we.find(gi,this._config.parent);return we.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=yi.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}fe.on(document,ui,_i,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of we.getMultipleElementsFromSelector(this))yi.getOrCreateInstance(t,{toggle:!1}).toggle()})),Qt(yi);const wi="dropdown",Ei=".bs.dropdown",Ai=".data-api",Ti="ArrowUp",Ci="ArrowDown",Oi=`hide${Ei}`,xi=`hidden${Ei}`,ki=`show${Ei}`,Li=`shown${Ei}`,Si=`click${Ei}${Ai}`,Di=`keydown${Ei}${Ai}`,$i=`keyup${Ei}${Ai}`,Ii="show",Ni='[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)',Pi=`${Ni}.${Ii}`,Mi=".dropdown-menu",ji=Kt()?"top-end":"top-start",Fi=Kt()?"top-start":"top-end",Hi=Kt()?"bottom-end":"bottom-start",Bi=Kt()?"bottom-start":"bottom-end",Wi=Kt()?"left-start":"right-start",zi=Kt()?"right-start":"left-start",Ri={autoClose:!0,boundary:"clippingParents",display:"dynamic",offset:[0,2],popperConfig:null,reference:"toggle"},qi={autoClose:"(boolean|string)",boundary:"(string|element)",display:"string",offset:"(array|string|function)",popperConfig:"(null|object|function)",reference:"(string|element|object)"};class Vi extends ve{constructor(t,e){super(t,e),this._popper=null,this._parent=this._element.parentNode,this._menu=we.next(this._element,Mi)[0]||we.prev(this._element,Mi)[0]||we.findOne(Mi,this._parent),this._inNavbar=this._detectNavbar()}static get Default(){return Ri}static get DefaultType(){return qi}static get NAME(){return wi}toggle(){return this._isShown()?this.hide():this.show()}show(){if(Wt(this._element)||this._isShown())return;const t={relatedTarget:this._element};if(!fe.trigger(this._element,ki,t).defaultPrevented){if(this._createPopper(),"ontouchstart"in document.documentElement&&!this._parent.closest(".navbar-nav"))for(const t of[].concat(...document.body.children))fe.on(t,"mouseover",Rt);this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(Ii),this._element.classList.add(Ii),fe.trigger(this._element,Li,t)}}hide(){if(Wt(this._element)||!this._isShown())return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(t){if(!fe.trigger(this._element,Oi,t).defaultPrevented){if("ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))fe.off(t,"mouseover",Rt);this._popper&&this._popper.destroy(),this._menu.classList.remove(Ii),this._element.classList.remove(Ii),this._element.setAttribute("aria-expanded","false"),_e.removeDataAttribute(this._menu,"popper"),fe.trigger(this._element,xi,t)}}_getConfig(t){if("object"==typeof(t=super._getConfig(t)).reference&&!Ft(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError(`${wi.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return t}_createPopper(){if(void 0===e)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org)");let t=this._element;"parent"===this._config.reference?t=this._parent:Ft(this._config.reference)?t=Ht(this._config.reference):"object"==typeof this._config.reference&&(t=this._config.reference);const i=this._getPopperConfig();this._popper=Dt(t,this._menu,i)}_isShown(){return this._menu.classList.contains(Ii)}_getPlacement(){const t=this._parent;if(t.classList.contains("dropend"))return Wi;if(t.classList.contains("dropstart"))return zi;if(t.classList.contains("dropup-center"))return"top";if(t.classList.contains("dropdown-center"))return"bottom";const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?Fi:ji:e?Bi:Hi}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(_e.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...Xt(this._config.popperConfig,[t])}}_selectMenuItem({key:t,target:e}){const i=we.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>Bt(t)));i.length&&Gt(i,e,t===Ci,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=Vi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=we.find(Pi);for(const i of e){const e=Vi.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Ti,Ci].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Ni)?this:we.prev(this,Ni)[0]||we.next(this,Ni)[0]||we.findOne(Ni,t.delegateTarget.parentNode),o=Vi.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}fe.on(document,Di,Ni,Vi.dataApiKeydownHandler),fe.on(document,Di,Mi,Vi.dataApiKeydownHandler),fe.on(document,Si,Vi.clearMenus),fe.on(document,$i,Vi.clearMenus),fe.on(document,Si,Ni,(function(t){t.preventDefault(),Vi.getOrCreateInstance(this).toggle()})),Qt(Vi);const Yi="backdrop",Ki="show",Qi=`mousedown.bs.${Yi}`,Xi={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Ui={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Gi extends be{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Xi}static get DefaultType(){return Ui}static get NAME(){return Yi}show(t){if(!this._config.isVisible)return void Xt(t);this._append();const e=this._getElement();this._config.isAnimated&&qt(e),e.classList.add(Ki),this._emulateAnimation((()=>{Xt(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(Ki),this._emulateAnimation((()=>{this.dispose(),Xt(t)}))):Xt(t)}dispose(){this._isAppended&&(fe.off(this._element,Qi),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=Ht(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),fe.on(t,Qi,(()=>{Xt(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){Ut(t,this._getElement(),this._config.isAnimated)}}const Ji=".bs.focustrap",Zi=`focusin${Ji}`,tn=`keydown.tab${Ji}`,en="backward",nn={autofocus:!0,trapElement:null},sn={autofocus:"boolean",trapElement:"element"};class on extends be{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return nn}static get DefaultType(){return sn}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),fe.off(document,Ji),fe.on(document,Zi,(t=>this._handleFocusin(t))),fe.on(document,tn,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,fe.off(document,Ji))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=we.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===en?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?en:"forward")}}const rn=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",an=".sticky-top",ln="padding-right",cn="margin-right";class hn{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,ln,(e=>e+t)),this._setElementAttributes(rn,ln,(e=>e+t)),this._setElementAttributes(an,cn,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,ln),this._resetElementAttributes(rn,ln),this._resetElementAttributes(an,cn)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&_e.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=_e.getDataAttribute(t,e);null!==i?(_e.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(Ft(t))e(t);else for(const i of we.find(t,this._element))e(i)}}const dn=".bs.modal",un=`hide${dn}`,fn=`hidePrevented${dn}`,pn=`hidden${dn}`,mn=`show${dn}`,gn=`shown${dn}`,_n=`resize${dn}`,bn=`click.dismiss${dn}`,vn=`mousedown.dismiss${dn}`,yn=`keydown.dismiss${dn}`,wn=`click${dn}.data-api`,En="modal-open",An="show",Tn="modal-static",Cn={backdrop:!0,focus:!0,keyboard:!0},On={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class xn extends ve{constructor(t,e){super(t,e),this._dialog=we.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new hn,this._addEventListeners()}static get Default(){return Cn}static get DefaultType(){return On}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||fe.trigger(this._element,mn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(En),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(fe.trigger(this._element,un).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(An),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){fe.off(window,dn),fe.off(this._dialog,dn),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Gi({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new on({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=we.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),qt(this._element),this._element.classList.add(An),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,fe.trigger(this._element,gn,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){fe.on(this._element,yn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),fe.on(window,_n,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),fe.on(this._element,vn,(t=>{fe.one(this._element,bn,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(En),this._resetAdjustments(),this._scrollBar.reset(),fe.trigger(this._element,pn)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(fe.trigger(this._element,fn).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(Tn)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(Tn),this._queueCallback((()=>{this._element.classList.remove(Tn),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=Kt()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=Kt()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=xn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}fe.on(document,wn,'[data-bs-toggle="modal"]',(function(t){const e=we.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),fe.one(e,mn,(t=>{t.defaultPrevented||fe.one(e,pn,(()=>{Bt(this)&&this.focus()}))}));const i=we.findOne(".modal.show");i&&xn.getInstance(i).hide(),xn.getOrCreateInstance(e).toggle(this)})),Ee(xn),Qt(xn);const kn=".bs.offcanvas",Ln=".data-api",Sn=`load${kn}${Ln}`,Dn="show",$n="showing",In="hiding",Nn=".offcanvas.show",Pn=`show${kn}`,Mn=`shown${kn}`,jn=`hide${kn}`,Fn=`hidePrevented${kn}`,Hn=`hidden${kn}`,Bn=`resize${kn}`,Wn=`click${kn}${Ln}`,zn=`keydown.dismiss${kn}`,Rn={backdrop:!0,keyboard:!0,scroll:!1},qn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class Vn extends ve{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return Rn}static get DefaultType(){return qn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||fe.trigger(this._element,Pn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new hn).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add($n),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(Dn),this._element.classList.remove($n),fe.trigger(this._element,Mn,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(fe.trigger(this._element,jn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(In),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(Dn,In),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new hn).reset(),fe.trigger(this._element,Hn)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Gi({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():fe.trigger(this._element,Fn)}:null})}_initializeFocusTrap(){return new on({trapElement:this._element})}_addEventListeners(){fe.on(this._element,zn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():fe.trigger(this._element,Fn))}))}static jQueryInterface(t){return this.each((function(){const e=Vn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}fe.on(document,Wn,'[data-bs-toggle="offcanvas"]',(function(t){const e=we.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),Wt(this))return;fe.one(e,Hn,(()=>{Bt(this)&&this.focus()}));const i=we.findOne(Nn);i&&i!==e&&Vn.getInstance(i).hide(),Vn.getOrCreateInstance(e).toggle(this)})),fe.on(window,Sn,(()=>{for(const t of we.find(Nn))Vn.getOrCreateInstance(t).show()})),fe.on(window,Bn,(()=>{for(const t of we.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&Vn.getOrCreateInstance(t).hide()})),Ee(Vn),Qt(Vn);const Yn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Kn=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Qn=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Xn=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Kn.has(i)||Boolean(Qn.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Un={allowList:Yn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Gn={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Jn={entry:"(string|element|function|null)",selector:"(string|element)"};class Zn extends be{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Un}static get DefaultType(){return Gn}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Jn)}_setContent(t,e,i){const n=we.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?Ft(e)?this._putElementInTemplate(Ht(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Xn(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return Xt(t,[this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const ts=new Set(["sanitize","allowList","sanitizeFn"]),es="fade",is="show",ns=".modal",ss="hide.bs.modal",os="hover",rs="focus",as={AUTO:"auto",TOP:"top",RIGHT:Kt()?"left":"right",BOTTOM:"bottom",LEFT:Kt()?"right":"left"},ls={allowList:Yn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},cs={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class hs extends ve{constructor(t,i){if(void 0===e)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,i),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return ls}static get DefaultType(){return cs}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),fe.off(this._element.closest(ns),ss,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=fe.trigger(this._element,this.constructor.eventName("show")),e=(zt(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),fe.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(is),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))fe.on(t,"mouseover",Rt);this._queueCallback((()=>{fe.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!fe.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(is),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))fe.off(t,"mouseover",Rt);this._activeTrigger.click=!1,this._activeTrigger[rs]=!1,this._activeTrigger[os]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),fe.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(es,is),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(es),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Zn({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{".tooltip-inner":this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(es)}_isShown(){return this.tip&&this.tip.classList.contains(is)}_createPopper(t){const e=Xt(this._config.placement,[this,t,this._element]),i=as[e.toUpperCase()];return Dt(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return Xt(t,[this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...Xt(this._config.popperConfig,[e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)fe.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===os?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===os?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");fe.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?rs:os]=!0,e._enter()})),fe.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?rs:os]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},fe.on(this._element.closest(ns),ss,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=_e.getDataAttributes(this._element);for(const t of Object.keys(e))ts.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:Ht(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=hs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}Qt(hs);const ds={...hs.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},us={...hs.DefaultType,content:"(null|string|element|function)"};class fs extends hs{static get Default(){return ds}static get DefaultType(){return us}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{".popover-header":this._getTitle(),".popover-body":this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=fs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}Qt(fs);const ps=".bs.scrollspy",ms=`activate${ps}`,gs=`click${ps}`,_s=`load${ps}.data-api`,bs="active",vs="[href]",ys=".nav-link",ws=`${ys}, .nav-item > ${ys}, .list-group-item`,Es={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},As={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Ts extends ve{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return Es}static get DefaultType(){return As}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=Ht(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(fe.off(this._config.target,gs),fe.on(this._config.target,gs,vs,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=we.find(vs,this._config.target);for(const e of t){if(!e.hash||Wt(e))continue;const t=we.findOne(decodeURI(e.hash),this._element);Bt(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(bs),this._activateParents(t),fe.trigger(this._element,ms,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))we.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(bs);else for(const e of we.parents(t,".nav, .list-group"))for(const t of we.prev(e,ws))t.classList.add(bs)}_clearActiveClass(t){t.classList.remove(bs);const e=we.find(`${vs}.${bs}`,t);for(const t of e)t.classList.remove(bs)}static jQueryInterface(t){return this.each((function(){const e=Ts.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}fe.on(window,_s,(()=>{for(const t of we.find('[data-bs-spy="scroll"]'))Ts.getOrCreateInstance(t)})),Qt(Ts);const Cs=".bs.tab",Os=`hide${Cs}`,xs=`hidden${Cs}`,ks=`show${Cs}`,Ls=`shown${Cs}`,Ss=`click${Cs}`,Ds=`keydown${Cs}`,$s=`load${Cs}`,Is="ArrowLeft",Ns="ArrowRight",Ps="ArrowUp",Ms="ArrowDown",js="Home",Fs="End",Hs="active",Bs="fade",Ws="show",zs=".dropdown-toggle",Rs=`:not(${zs})`,qs='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',Vs=`.nav-link${Rs}, .list-group-item${Rs}, [role="tab"]${Rs}, ${qs}`,Ys=`.${Hs}[data-bs-toggle="tab"], .${Hs}[data-bs-toggle="pill"], .${Hs}[data-bs-toggle="list"]`;class Ks extends ve{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),fe.on(this._element,Ds,(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?fe.trigger(e,Os,{relatedTarget:t}):null;fe.trigger(t,ks,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(Hs),this._activate(we.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),fe.trigger(t,Ls,{relatedTarget:e})):t.classList.add(Ws)}),t,t.classList.contains(Bs)))}_deactivate(t,e){t&&(t.classList.remove(Hs),t.blur(),this._deactivate(we.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),fe.trigger(t,xs,{relatedTarget:e})):t.classList.remove(Ws)}),t,t.classList.contains(Bs)))}_keydown(t){if(![Is,Ns,Ps,Ms,js,Fs].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!Wt(t)));let i;if([js,Fs].includes(t.key))i=e[t.key===js?0:e.length-1];else{const n=[Ns,Ms].includes(t.key);i=Gt(e,t.target,n,!0)}i&&(i.focus({preventScroll:!0}),Ks.getOrCreateInstance(i).show())}_getChildren(){return we.find(Vs,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=we.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=we.findOne(t,i);s&&s.classList.toggle(n,e)};n(zs,Hs),n(".dropdown-menu",Ws),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(Hs)}_getInnerElement(t){return t.matches(Vs)?t:we.findOne(Vs,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=Ks.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}fe.on(document,Ss,qs,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),Wt(this)||Ks.getOrCreateInstance(this).show()})),fe.on(window,$s,(()=>{for(const t of we.find(Ys))Ks.getOrCreateInstance(t)})),Qt(Ks);const Qs=".bs.toast",Xs=`mouseover${Qs}`,Us=`mouseout${Qs}`,Gs=`focusin${Qs}`,Js=`focusout${Qs}`,Zs=`hide${Qs}`,to=`hidden${Qs}`,eo=`show${Qs}`,io=`shown${Qs}`,no="hide",so="show",oo="showing",ro={animation:"boolean",autohide:"boolean",delay:"number"},ao={animation:!0,autohide:!0,delay:5e3};class lo extends ve{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return ao}static get DefaultType(){return ro}static get NAME(){return"toast"}show(){fe.trigger(this._element,eo).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(no),qt(this._element),this._element.classList.add(so,oo),this._queueCallback((()=>{this._element.classList.remove(oo),fe.trigger(this._element,io),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(fe.trigger(this._element,Zs).defaultPrevented||(this._element.classList.add(oo),this._queueCallback((()=>{this._element.classList.add(no),this._element.classList.remove(oo,so),fe.trigger(this._element,to)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(so),super.dispose()}isShown(){return this._element.classList.contains(so)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){fe.on(this._element,Xs,(t=>this._onInteraction(t,!0))),fe.on(this._element,Us,(t=>this._onInteraction(t,!1))),fe.on(this._element,Gs,(t=>this._onInteraction(t,!0))),fe.on(this._element,Js,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=lo.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}function co(t){"loading"!=document.readyState?t():document.addEventListener("DOMContentLoaded",t)}Ee(lo),Qt(lo),co((function(){[].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')).map((function(t){return new hs(t,{delay:{show:500,hide:100}})}))})),co((function(){document.getElementById("pst-back-to-top").addEventListener("click",(function(){document.body.scrollTop=0,document.documentElement.scrollTop=0}))})),co((function(){var t=document.getElementById("pst-back-to-top"),e=document.getElementsByClassName("bd-header")[0].getBoundingClientRect();window.addEventListener("scroll",(function(){this.oldScroll>this.scrollY&&this.scrollY>e.bottom?t.style.display="block":t.style.display="none",this.oldScroll=this.scrollY}))})),window.bootstrap=i})(); +//# sourceMappingURL=bootstrap.js.map \ No newline at end of file diff --git a/main/_static/scripts/bootstrap.js.LICENSE.txt b/main/_static/scripts/bootstrap.js.LICENSE.txt new file mode 100644 index 0000000..10f979d --- /dev/null +++ b/main/_static/scripts/bootstrap.js.LICENSE.txt @@ -0,0 +1,5 @@ +/*! + * Bootstrap v5.3.2 (https://getbootstrap.com/) + * Copyright 2011-2023 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ diff --git a/main/_static/scripts/bootstrap.js.map b/main/_static/scripts/bootstrap.js.map new file mode 100644 index 0000000..64e212b --- /dev/null +++ b/main/_static/scripts/bootstrap.js.map @@ -0,0 +1 @@ +{"version":3,"file":"scripts/bootstrap.js","mappings":";mBACA,IAAIA,EAAsB,CCA1BA,EAAwB,CAACC,EAASC,KACjC,IAAI,IAAIC,KAAOD,EACXF,EAAoBI,EAAEF,EAAYC,KAASH,EAAoBI,EAAEH,EAASE,IAC5EE,OAAOC,eAAeL,EAASE,EAAK,CAAEI,YAAY,EAAMC,IAAKN,EAAWC,IAE1E,ECNDH,EAAwB,CAACS,EAAKC,IAAUL,OAAOM,UAAUC,eAAeC,KAAKJ,EAAKC,GCClFV,EAAyBC,IACH,oBAAXa,QAA0BA,OAAOC,aAC1CV,OAAOC,eAAeL,EAASa,OAAOC,YAAa,CAAEC,MAAO,WAE7DX,OAAOC,eAAeL,EAAS,aAAc,CAAEe,OAAO,GAAO,01BCLvD,IAAI,EAAM,MACNC,EAAS,SACTC,EAAQ,QACRC,EAAO,OACPC,EAAO,OACPC,EAAiB,CAAC,EAAKJ,EAAQC,EAAOC,GACtCG,EAAQ,QACRC,EAAM,MACNC,EAAkB,kBAClBC,EAAW,WACXC,EAAS,SACTC,EAAY,YACZC,EAAmCP,EAAeQ,QAAO,SAAUC,EAAKC,GACjF,OAAOD,EAAIE,OAAO,CAACD,EAAY,IAAMT,EAAOS,EAAY,IAAMR,GAChE,GAAG,IACQ,EAA0B,GAAGS,OAAOX,EAAgB,CAACD,IAAOS,QAAO,SAAUC,EAAKC,GAC3F,OAAOD,EAAIE,OAAO,CAACD,EAAWA,EAAY,IAAMT,EAAOS,EAAY,IAAMR,GAC3E,GAAG,IAEQU,EAAa,aACbC,EAAO,OACPC,EAAY,YAEZC,EAAa,aACbC,EAAO,OACPC,EAAY,YAEZC,EAAc,cACdC,EAAQ,QACRC,EAAa,aACbC,EAAiB,CAACT,EAAYC,EAAMC,EAAWC,EAAYC,EAAMC,EAAWC,EAAaC,EAAOC,GC9B5F,SAASE,EAAYC,GAClC,OAAOA,GAAWA,EAAQC,UAAY,IAAIC,cAAgB,IAC5D,CCFe,SAASC,EAAUC,GAChC,GAAY,MAARA,EACF,OAAOC,OAGT,GAAwB,oBAApBD,EAAKE,WAAkC,CACzC,IAAIC,EAAgBH,EAAKG,cACzB,OAAOA,GAAgBA,EAAcC,aAAwBH,MAC/D,CAEA,OAAOD,CACT,CCTA,SAASK,EAAUL,GAEjB,OAAOA,aADUD,EAAUC,GAAMM,SACIN,aAAgBM,OACvD,CAEA,SAASC,EAAcP,GAErB,OAAOA,aADUD,EAAUC,GAAMQ,aACIR,aAAgBQ,WACvD,CAEA,SAASC,EAAaT,GAEpB,MAA0B,oBAAfU,aAKJV,aADUD,EAAUC,GAAMU,YACIV,aAAgBU,WACvD,CCwDA,SACEC,KAAM,cACNC,SAAS,EACTC,MAAO,QACPC,GA5EF,SAAqBC,GACnB,IAAIC,EAAQD,EAAKC,MACjB3D,OAAO4D,KAAKD,EAAME,UAAUC,SAAQ,SAAUR,GAC5C,IAAIS,EAAQJ,EAAMK,OAAOV,IAAS,CAAC,EAC/BW,EAAaN,EAAMM,WAAWX,IAAS,CAAC,EACxCf,EAAUoB,EAAME,SAASP,GAExBJ,EAAcX,IAAaD,EAAYC,KAO5CvC,OAAOkE,OAAO3B,EAAQwB,MAAOA,GAC7B/D,OAAO4D,KAAKK,GAAYH,SAAQ,SAAUR,GACxC,IAAI3C,EAAQsD,EAAWX,IAET,IAAV3C,EACF4B,EAAQ4B,gBAAgBb,GAExBf,EAAQ6B,aAAad,GAAgB,IAAV3C,EAAiB,GAAKA,EAErD,IACF,GACF,EAoDE0D,OAlDF,SAAgBC,GACd,IAAIX,EAAQW,EAAMX,MACdY,EAAgB,CAClBlD,OAAQ,CACNmD,SAAUb,EAAMc,QAAQC,SACxB5D,KAAM,IACN6D,IAAK,IACLC,OAAQ,KAEVC,MAAO,CACLL,SAAU,YAEZlD,UAAW,CAAC,GASd,OAPAtB,OAAOkE,OAAOP,EAAME,SAASxC,OAAO0C,MAAOQ,EAAclD,QACzDsC,EAAMK,OAASO,EAEXZ,EAAME,SAASgB,OACjB7E,OAAOkE,OAAOP,EAAME,SAASgB,MAAMd,MAAOQ,EAAcM,OAGnD,WACL7E,OAAO4D,KAAKD,EAAME,UAAUC,SAAQ,SAAUR,GAC5C,IAAIf,EAAUoB,EAAME,SAASP,GACzBW,EAAaN,EAAMM,WAAWX,IAAS,CAAC,EAGxCS,EAFkB/D,OAAO4D,KAAKD,EAAMK,OAAOzD,eAAe+C,GAAQK,EAAMK,OAAOV,GAAQiB,EAAcjB,IAE7E9B,QAAO,SAAUuC,EAAOe,GAElD,OADAf,EAAMe,GAAY,GACXf,CACT,GAAG,CAAC,GAECb,EAAcX,IAAaD,EAAYC,KAI5CvC,OAAOkE,OAAO3B,EAAQwB,MAAOA,GAC7B/D,OAAO4D,KAAKK,GAAYH,SAAQ,SAAUiB,GACxCxC,EAAQ4B,gBAAgBY,EAC1B,IACF,GACF,CACF,EASEC,SAAU,CAAC,kBCjFE,SAASC,EAAiBvD,GACvC,OAAOA,EAAUwD,MAAM,KAAK,EAC9B,CCHO,IAAI,EAAMC,KAAKC,IACX,EAAMD,KAAKE,IACXC,EAAQH,KAAKG,MCFT,SAASC,IACtB,IAAIC,EAASC,UAAUC,cAEvB,OAAc,MAAVF,GAAkBA,EAAOG,QAAUC,MAAMC,QAAQL,EAAOG,QACnDH,EAAOG,OAAOG,KAAI,SAAUC,GACjC,OAAOA,EAAKC,MAAQ,IAAMD,EAAKE,OACjC,IAAGC,KAAK,KAGHT,UAAUU,SACnB,CCTe,SAASC,IACtB,OAAQ,iCAAiCC,KAAKd,IAChD,CCCe,SAASe,EAAsB/D,EAASgE,EAAcC,QAC9C,IAAjBD,IACFA,GAAe,QAGO,IAApBC,IACFA,GAAkB,GAGpB,IAAIC,EAAalE,EAAQ+D,wBACrBI,EAAS,EACTC,EAAS,EAETJ,GAAgBrD,EAAcX,KAChCmE,EAASnE,EAAQqE,YAAc,GAAItB,EAAMmB,EAAWI,OAAStE,EAAQqE,aAAmB,EACxFD,EAASpE,EAAQuE,aAAe,GAAIxB,EAAMmB,EAAWM,QAAUxE,EAAQuE,cAAoB,GAG7F,IACIE,GADOhE,EAAUT,GAAWG,EAAUH,GAAWK,QAC3BoE,eAEtBC,GAAoBb,KAAsBI,EAC1CU,GAAKT,EAAW3F,MAAQmG,GAAoBD,EAAiBA,EAAeG,WAAa,IAAMT,EAC/FU,GAAKX,EAAW9B,KAAOsC,GAAoBD,EAAiBA,EAAeK,UAAY,IAAMV,EAC7FE,EAAQJ,EAAWI,MAAQH,EAC3BK,EAASN,EAAWM,OAASJ,EACjC,MAAO,CACLE,MAAOA,EACPE,OAAQA,EACRpC,IAAKyC,EACLvG,MAAOqG,EAAIL,EACXjG,OAAQwG,EAAIL,EACZjG,KAAMoG,EACNA,EAAGA,EACHE,EAAGA,EAEP,CCrCe,SAASE,EAAc/E,GACpC,IAAIkE,EAAaH,EAAsB/D,GAGnCsE,EAAQtE,EAAQqE,YAChBG,EAASxE,EAAQuE,aAUrB,OARI3B,KAAKoC,IAAId,EAAWI,MAAQA,IAAU,IACxCA,EAAQJ,EAAWI,OAGjB1B,KAAKoC,IAAId,EAAWM,OAASA,IAAW,IAC1CA,EAASN,EAAWM,QAGf,CACLG,EAAG3E,EAAQ4E,WACXC,EAAG7E,EAAQ8E,UACXR,MAAOA,EACPE,OAAQA,EAEZ,CCvBe,SAASS,EAASC,EAAQC,GACvC,IAAIC,EAAWD,EAAME,aAAeF,EAAME,cAE1C,GAAIH,EAAOD,SAASE,GAClB,OAAO,EAEJ,GAAIC,GAAYvE,EAAauE,GAAW,CACzC,IAAIE,EAAOH,EAEX,EAAG,CACD,GAAIG,GAAQJ,EAAOK,WAAWD,GAC5B,OAAO,EAITA,EAAOA,EAAKE,YAAcF,EAAKG,IACjC,OAASH,EACX,CAGF,OAAO,CACT,CCrBe,SAAS,EAAiBtF,GACvC,OAAOG,EAAUH,GAAS0F,iBAAiB1F,EAC7C,CCFe,SAAS2F,EAAe3F,GACrC,MAAO,CAAC,QAAS,KAAM,MAAM4F,QAAQ7F,EAAYC,KAAa,CAChE,CCFe,SAAS6F,EAAmB7F,GAEzC,QAASS,EAAUT,GAAWA,EAAQO,cACtCP,EAAQ8F,WAAazF,OAAOyF,UAAUC,eACxC,CCFe,SAASC,EAAchG,GACpC,MAA6B,SAAzBD,EAAYC,GACPA,EAMPA,EAAQiG,cACRjG,EAAQwF,aACR3E,EAAab,GAAWA,EAAQyF,KAAO,OAEvCI,EAAmB7F,EAGvB,CCVA,SAASkG,EAAoBlG,GAC3B,OAAKW,EAAcX,IACoB,UAAvC,EAAiBA,GAASiC,SAInBjC,EAAQmG,aAHN,IAIX,CAwCe,SAASC,EAAgBpG,GAItC,IAHA,IAAIK,EAASF,EAAUH,GACnBmG,EAAeD,EAAoBlG,GAEhCmG,GAAgBR,EAAeQ,IAA6D,WAA5C,EAAiBA,GAAclE,UACpFkE,EAAeD,EAAoBC,GAGrC,OAAIA,IAA+C,SAA9BpG,EAAYoG,IAA0D,SAA9BpG,EAAYoG,IAAwE,WAA5C,EAAiBA,GAAclE,UAC3H5B,EAGF8F,GAhDT,SAA4BnG,GAC1B,IAAIqG,EAAY,WAAWvC,KAAKd,KAGhC,GAFW,WAAWc,KAAKd,MAEfrC,EAAcX,IAII,UAFX,EAAiBA,GAEnBiC,SACb,OAAO,KAIX,IAAIqE,EAAcN,EAAchG,GAMhC,IAJIa,EAAayF,KACfA,EAAcA,EAAYb,MAGrB9E,EAAc2F,IAAgB,CAAC,OAAQ,QAAQV,QAAQ7F,EAAYuG,IAAgB,GAAG,CAC3F,IAAIC,EAAM,EAAiBD,GAI3B,GAAsB,SAAlBC,EAAIC,WAA4C,SAApBD,EAAIE,aAA0C,UAAhBF,EAAIG,UAAiF,IAA1D,CAAC,YAAa,eAAed,QAAQW,EAAII,aAAsBN,GAAgC,WAAnBE,EAAII,YAA2BN,GAAaE,EAAIK,QAAyB,SAAfL,EAAIK,OACjO,OAAON,EAEPA,EAAcA,EAAYd,UAE9B,CAEA,OAAO,IACT,CAgByBqB,CAAmB7G,IAAYK,CACxD,CCpEe,SAASyG,EAAyB3H,GAC/C,MAAO,CAAC,MAAO,UAAUyG,QAAQzG,IAAc,EAAI,IAAM,GAC3D,CCDO,SAAS4H,EAAOjE,EAAK1E,EAAOyE,GACjC,OAAO,EAAQC,EAAK,EAAQ1E,EAAOyE,GACrC,CCFe,SAASmE,EAAmBC,GACzC,OAAOxJ,OAAOkE,OAAO,CAAC,ECDf,CACLS,IAAK,EACL9D,MAAO,EACPD,OAAQ,EACRE,KAAM,GDHuC0I,EACjD,CEHe,SAASC,EAAgB9I,EAAOiD,GAC7C,OAAOA,EAAKpC,QAAO,SAAUkI,EAAS5J,GAEpC,OADA4J,EAAQ5J,GAAOa,EACR+I,CACT,GAAG,CAAC,EACN,CC4EA,SACEpG,KAAM,QACNC,SAAS,EACTC,MAAO,OACPC,GApEF,SAAeC,GACb,IAAIiG,EAEAhG,EAAQD,EAAKC,MACbL,EAAOI,EAAKJ,KACZmB,EAAUf,EAAKe,QACfmF,EAAejG,EAAME,SAASgB,MAC9BgF,EAAgBlG,EAAMmG,cAAcD,cACpCE,EAAgB9E,EAAiBtB,EAAMjC,WACvCsI,EAAOX,EAAyBU,GAEhCE,EADa,CAACnJ,EAAMD,GAAOsH,QAAQ4B,IAAkB,EAClC,SAAW,QAElC,GAAKH,GAAiBC,EAAtB,CAIA,IAAIL,EAxBgB,SAAyBU,EAASvG,GAItD,OAAO4F,EAAsC,iBAH7CW,EAA6B,mBAAZA,EAAyBA,EAAQlK,OAAOkE,OAAO,CAAC,EAAGP,EAAMwG,MAAO,CAC/EzI,UAAWiC,EAAMjC,aACbwI,GACkDA,EAAUT,EAAgBS,EAASlJ,GAC7F,CAmBsBoJ,CAAgB3F,EAAQyF,QAASvG,GACjD0G,EAAY/C,EAAcsC,GAC1BU,EAAmB,MAATN,EAAe,EAAMlJ,EAC/ByJ,EAAmB,MAATP,EAAepJ,EAASC,EAClC2J,EAAU7G,EAAMwG,MAAM7I,UAAU2I,GAAOtG,EAAMwG,MAAM7I,UAAU0I,GAAQH,EAAcG,GAAQrG,EAAMwG,MAAM9I,OAAO4I,GAC9GQ,EAAYZ,EAAcG,GAAQrG,EAAMwG,MAAM7I,UAAU0I,GACxDU,EAAoB/B,EAAgBiB,GACpCe,EAAaD,EAA6B,MAATV,EAAeU,EAAkBE,cAAgB,EAAIF,EAAkBG,aAAe,EAAI,EAC3HC,EAAoBN,EAAU,EAAIC,EAAY,EAG9CpF,EAAMmE,EAAcc,GACpBlF,EAAMuF,EAAaN,EAAUJ,GAAOT,EAAce,GAClDQ,EAASJ,EAAa,EAAIN,EAAUJ,GAAO,EAAIa,EAC/CE,EAAS1B,EAAOjE,EAAK0F,EAAQ3F,GAE7B6F,EAAWjB,EACfrG,EAAMmG,cAAcxG,KAASqG,EAAwB,CAAC,GAAyBsB,GAAYD,EAAQrB,EAAsBuB,aAAeF,EAASD,EAAQpB,EAnBzJ,CAoBF,EAkCEtF,OAhCF,SAAgBC,GACd,IAAIX,EAAQW,EAAMX,MAEdwH,EADU7G,EAAMG,QACWlC,QAC3BqH,OAAoC,IAArBuB,EAA8B,sBAAwBA,EAErD,MAAhBvB,IAKwB,iBAAjBA,IACTA,EAAejG,EAAME,SAASxC,OAAO+J,cAAcxB,MAOhDpC,EAAS7D,EAAME,SAASxC,OAAQuI,KAIrCjG,EAAME,SAASgB,MAAQ+E,EACzB,EASE5E,SAAU,CAAC,iBACXqG,iBAAkB,CAAC,oBCxFN,SAASC,EAAa5J,GACnC,OAAOA,EAAUwD,MAAM,KAAK,EAC9B,CCOA,IAAIqG,GAAa,CACf5G,IAAK,OACL9D,MAAO,OACPD,OAAQ,OACRE,KAAM,QAeD,SAAS0K,GAAYlH,GAC1B,IAAImH,EAEApK,EAASiD,EAAMjD,OACfqK,EAAapH,EAAMoH,WACnBhK,EAAY4C,EAAM5C,UAClBiK,EAAYrH,EAAMqH,UAClBC,EAAUtH,EAAMsH,QAChBpH,EAAWF,EAAME,SACjBqH,EAAkBvH,EAAMuH,gBACxBC,EAAWxH,EAAMwH,SACjBC,EAAezH,EAAMyH,aACrBC,EAAU1H,EAAM0H,QAChBC,EAAaL,EAAQ1E,EACrBA,OAAmB,IAAf+E,EAAwB,EAAIA,EAChCC,EAAaN,EAAQxE,EACrBA,OAAmB,IAAf8E,EAAwB,EAAIA,EAEhCC,EAAgC,mBAAjBJ,EAA8BA,EAAa,CAC5D7E,EAAGA,EACHE,IACG,CACHF,EAAGA,EACHE,GAGFF,EAAIiF,EAAMjF,EACVE,EAAI+E,EAAM/E,EACV,IAAIgF,EAAOR,EAAQrL,eAAe,KAC9B8L,EAAOT,EAAQrL,eAAe,KAC9B+L,EAAQxL,EACRyL,EAAQ,EACRC,EAAM5J,OAEV,GAAIkJ,EAAU,CACZ,IAAIpD,EAAeC,EAAgBtH,GAC/BoL,EAAa,eACbC,EAAY,cAEZhE,IAAiBhG,EAAUrB,IAGmB,WAA5C,EAFJqH,EAAeN,EAAmB/G,IAECmD,UAAsC,aAAbA,IAC1DiI,EAAa,eACbC,EAAY,gBAOZhL,IAAc,IAAQA,IAAcZ,GAAQY,IAAcb,IAAU8K,IAAczK,KACpFqL,EAAQ3L,EAGRwG,IAFc4E,GAAWtD,IAAiB8D,GAAOA,EAAIxF,eAAiBwF,EAAIxF,eAAeD,OACzF2B,EAAa+D,IACEf,EAAW3E,OAC1BK,GAAKyE,EAAkB,GAAK,GAG1BnK,IAAcZ,IAASY,IAAc,GAAOA,IAAcd,GAAW+K,IAAczK,KACrFoL,EAAQzL,EAGRqG,IAFc8E,GAAWtD,IAAiB8D,GAAOA,EAAIxF,eAAiBwF,EAAIxF,eAAeH,MACzF6B,EAAagE,IACEhB,EAAW7E,MAC1BK,GAAK2E,EAAkB,GAAK,EAEhC,CAEA,IAgBMc,EAhBFC,EAAe5M,OAAOkE,OAAO,CAC/BM,SAAUA,GACTsH,GAAYP,IAEXsB,GAAyB,IAAjBd,EAlFd,SAA2BrI,EAAM8I,GAC/B,IAAItF,EAAIxD,EAAKwD,EACTE,EAAI1D,EAAK0D,EACT0F,EAAMN,EAAIO,kBAAoB,EAClC,MAAO,CACL7F,EAAG5B,EAAM4B,EAAI4F,GAAOA,GAAO,EAC3B1F,EAAG9B,EAAM8B,EAAI0F,GAAOA,GAAO,EAE/B,CA0EsCE,CAAkB,CACpD9F,EAAGA,EACHE,GACC1E,EAAUrB,IAAW,CACtB6F,EAAGA,EACHE,GAMF,OAHAF,EAAI2F,EAAM3F,EACVE,EAAIyF,EAAMzF,EAENyE,EAGK7L,OAAOkE,OAAO,CAAC,EAAG0I,IAAeD,EAAiB,CAAC,GAAkBJ,GAASF,EAAO,IAAM,GAAIM,EAAeL,GAASF,EAAO,IAAM,GAAIO,EAAe5D,WAAayD,EAAIO,kBAAoB,IAAM,EAAI,aAAe7F,EAAI,OAASE,EAAI,MAAQ,eAAiBF,EAAI,OAASE,EAAI,SAAUuF,IAG5R3M,OAAOkE,OAAO,CAAC,EAAG0I,IAAenB,EAAkB,CAAC,GAAmBc,GAASF,EAAOjF,EAAI,KAAO,GAAIqE,EAAgBa,GAASF,EAAOlF,EAAI,KAAO,GAAIuE,EAAgB1C,UAAY,GAAI0C,GAC9L,CA4CA,UACEnI,KAAM,gBACNC,SAAS,EACTC,MAAO,cACPC,GA9CF,SAAuBwJ,GACrB,IAAItJ,EAAQsJ,EAAMtJ,MACdc,EAAUwI,EAAMxI,QAChByI,EAAwBzI,EAAQoH,gBAChCA,OAA4C,IAA1BqB,GAA0CA,EAC5DC,EAAoB1I,EAAQqH,SAC5BA,OAAiC,IAAtBqB,GAAsCA,EACjDC,EAAwB3I,EAAQsH,aAChCA,OAAyC,IAA1BqB,GAA0CA,EACzDR,EAAe,CACjBlL,UAAWuD,EAAiBtB,EAAMjC,WAClCiK,UAAWL,EAAa3H,EAAMjC,WAC9BL,OAAQsC,EAAME,SAASxC,OACvBqK,WAAY/H,EAAMwG,MAAM9I,OACxBwK,gBAAiBA,EACjBG,QAAoC,UAA3BrI,EAAMc,QAAQC,UAGgB,MAArCf,EAAMmG,cAAcD,gBACtBlG,EAAMK,OAAO3C,OAASrB,OAAOkE,OAAO,CAAC,EAAGP,EAAMK,OAAO3C,OAAQmK,GAAYxL,OAAOkE,OAAO,CAAC,EAAG0I,EAAc,CACvGhB,QAASjI,EAAMmG,cAAcD,cAC7BrF,SAAUb,EAAMc,QAAQC,SACxBoH,SAAUA,EACVC,aAAcA,OAIe,MAA7BpI,EAAMmG,cAAcjF,QACtBlB,EAAMK,OAAOa,MAAQ7E,OAAOkE,OAAO,CAAC,EAAGP,EAAMK,OAAOa,MAAO2G,GAAYxL,OAAOkE,OAAO,CAAC,EAAG0I,EAAc,CACrGhB,QAASjI,EAAMmG,cAAcjF,MAC7BL,SAAU,WACVsH,UAAU,EACVC,aAAcA,OAIlBpI,EAAMM,WAAW5C,OAASrB,OAAOkE,OAAO,CAAC,EAAGP,EAAMM,WAAW5C,OAAQ,CACnE,wBAAyBsC,EAAMjC,WAEnC,EAQE2L,KAAM,CAAC,GCrKT,IAAIC,GAAU,CACZA,SAAS,GAsCX,UACEhK,KAAM,iBACNC,SAAS,EACTC,MAAO,QACPC,GAAI,WAAe,EACnBY,OAxCF,SAAgBX,GACd,IAAIC,EAAQD,EAAKC,MACb4J,EAAW7J,EAAK6J,SAChB9I,EAAUf,EAAKe,QACf+I,EAAkB/I,EAAQgJ,OAC1BA,OAA6B,IAApBD,GAAoCA,EAC7CE,EAAkBjJ,EAAQkJ,OAC1BA,OAA6B,IAApBD,GAAoCA,EAC7C9K,EAASF,EAAUiB,EAAME,SAASxC,QAClCuM,EAAgB,GAAGjM,OAAOgC,EAAMiK,cAActM,UAAWqC,EAAMiK,cAAcvM,QAYjF,OAVIoM,GACFG,EAAc9J,SAAQ,SAAU+J,GAC9BA,EAAaC,iBAAiB,SAAUP,EAASQ,OAAQT,GAC3D,IAGEK,GACF/K,EAAOkL,iBAAiB,SAAUP,EAASQ,OAAQT,IAG9C,WACDG,GACFG,EAAc9J,SAAQ,SAAU+J,GAC9BA,EAAaG,oBAAoB,SAAUT,EAASQ,OAAQT,GAC9D,IAGEK,GACF/K,EAAOoL,oBAAoB,SAAUT,EAASQ,OAAQT,GAE1D,CACF,EASED,KAAM,CAAC,GC/CT,IAAIY,GAAO,CACTnN,KAAM,QACND,MAAO,OACPD,OAAQ,MACR+D,IAAK,UAEQ,SAASuJ,GAAqBxM,GAC3C,OAAOA,EAAUyM,QAAQ,0BAA0B,SAAUC,GAC3D,OAAOH,GAAKG,EACd,GACF,CCVA,IAAI,GAAO,CACTnN,MAAO,MACPC,IAAK,SAEQ,SAASmN,GAA8B3M,GACpD,OAAOA,EAAUyM,QAAQ,cAAc,SAAUC,GAC/C,OAAO,GAAKA,EACd,GACF,CCPe,SAASE,GAAgB3L,GACtC,IAAI6J,EAAM9J,EAAUC,GAGpB,MAAO,CACL4L,WAHe/B,EAAIgC,YAInBC,UAHcjC,EAAIkC,YAKtB,CCNe,SAASC,GAAoBpM,GAQ1C,OAAO+D,EAAsB8B,EAAmB7F,IAAUzB,KAAOwN,GAAgB/L,GAASgM,UAC5F,CCXe,SAASK,GAAerM,GAErC,IAAIsM,EAAoB,EAAiBtM,GACrCuM,EAAWD,EAAkBC,SAC7BC,EAAYF,EAAkBE,UAC9BC,EAAYH,EAAkBG,UAElC,MAAO,6BAA6B3I,KAAKyI,EAAWE,EAAYD,EAClE,CCLe,SAASE,GAAgBtM,GACtC,MAAI,CAAC,OAAQ,OAAQ,aAAawF,QAAQ7F,EAAYK,KAAU,EAEvDA,EAAKG,cAAcoM,KAGxBhM,EAAcP,IAASiM,GAAejM,GACjCA,EAGFsM,GAAgB1G,EAAc5F,GACvC,CCJe,SAASwM,GAAkB5M,EAAS6M,GACjD,IAAIC,OAES,IAATD,IACFA,EAAO,IAGT,IAAIvB,EAAeoB,GAAgB1M,GAC/B+M,EAASzB,KAAqE,OAAlDwB,EAAwB9M,EAAQO,oBAAyB,EAASuM,EAAsBH,MACpH1C,EAAM9J,EAAUmL,GAChB0B,EAASD,EAAS,CAAC9C,GAAK7K,OAAO6K,EAAIxF,gBAAkB,GAAI4H,GAAef,GAAgBA,EAAe,IAAMA,EAC7G2B,EAAcJ,EAAKzN,OAAO4N,GAC9B,OAAOD,EAASE,EAChBA,EAAY7N,OAAOwN,GAAkB5G,EAAcgH,IACrD,CCzBe,SAASE,GAAiBC,GACvC,OAAO1P,OAAOkE,OAAO,CAAC,EAAGwL,EAAM,CAC7B5O,KAAM4O,EAAKxI,EACXvC,IAAK+K,EAAKtI,EACVvG,MAAO6O,EAAKxI,EAAIwI,EAAK7I,MACrBjG,OAAQ8O,EAAKtI,EAAIsI,EAAK3I,QAE1B,CCqBA,SAAS4I,GAA2BpN,EAASqN,EAAgBlL,GAC3D,OAAOkL,IAAmBxO,EAAWqO,GCzBxB,SAAyBlN,EAASmC,GAC/C,IAAI8H,EAAM9J,EAAUH,GAChBsN,EAAOzH,EAAmB7F,GAC1ByE,EAAiBwF,EAAIxF,eACrBH,EAAQgJ,EAAKhF,YACb9D,EAAS8I,EAAKjF,aACd1D,EAAI,EACJE,EAAI,EAER,GAAIJ,EAAgB,CAClBH,EAAQG,EAAeH,MACvBE,EAASC,EAAeD,OACxB,IAAI+I,EAAiB1J,KAEjB0J,IAAmBA,GAA+B,UAAbpL,KACvCwC,EAAIF,EAAeG,WACnBC,EAAIJ,EAAeK,UAEvB,CAEA,MAAO,CACLR,MAAOA,EACPE,OAAQA,EACRG,EAAGA,EAAIyH,GAAoBpM,GAC3B6E,EAAGA,EAEP,CDDwD2I,CAAgBxN,EAASmC,IAAa1B,EAAU4M,GAdxG,SAAoCrN,EAASmC,GAC3C,IAAIgL,EAAOpJ,EAAsB/D,GAAS,EAAoB,UAAbmC,GASjD,OARAgL,EAAK/K,IAAM+K,EAAK/K,IAAMpC,EAAQyN,UAC9BN,EAAK5O,KAAO4O,EAAK5O,KAAOyB,EAAQ0N,WAChCP,EAAK9O,OAAS8O,EAAK/K,IAAMpC,EAAQqI,aACjC8E,EAAK7O,MAAQ6O,EAAK5O,KAAOyB,EAAQsI,YACjC6E,EAAK7I,MAAQtE,EAAQsI,YACrB6E,EAAK3I,OAASxE,EAAQqI,aACtB8E,EAAKxI,EAAIwI,EAAK5O,KACd4O,EAAKtI,EAAIsI,EAAK/K,IACP+K,CACT,CAG0HQ,CAA2BN,EAAgBlL,GAAY+K,GEtBlK,SAAyBlN,GACtC,IAAI8M,EAEAQ,EAAOzH,EAAmB7F,GAC1B4N,EAAY7B,GAAgB/L,GAC5B2M,EAA0D,OAAlDG,EAAwB9M,EAAQO,oBAAyB,EAASuM,EAAsBH,KAChGrI,EAAQ,EAAIgJ,EAAKO,YAAaP,EAAKhF,YAAaqE,EAAOA,EAAKkB,YAAc,EAAGlB,EAAOA,EAAKrE,YAAc,GACvG9D,EAAS,EAAI8I,EAAKQ,aAAcR,EAAKjF,aAAcsE,EAAOA,EAAKmB,aAAe,EAAGnB,EAAOA,EAAKtE,aAAe,GAC5G1D,GAAKiJ,EAAU5B,WAAaI,GAAoBpM,GAChD6E,GAAK+I,EAAU1B,UAMnB,MAJiD,QAA7C,EAAiBS,GAAQW,GAAMS,YACjCpJ,GAAK,EAAI2I,EAAKhF,YAAaqE,EAAOA,EAAKrE,YAAc,GAAKhE,GAGrD,CACLA,MAAOA,EACPE,OAAQA,EACRG,EAAGA,EACHE,EAAGA,EAEP,CFCkMmJ,CAAgBnI,EAAmB7F,IACrO,CG1Be,SAASiO,GAAe9M,GACrC,IAOIkI,EAPAtK,EAAYoC,EAAKpC,UACjBiB,EAAUmB,EAAKnB,QACfb,EAAYgC,EAAKhC,UACjBqI,EAAgBrI,EAAYuD,EAAiBvD,GAAa,KAC1DiK,EAAYjK,EAAY4J,EAAa5J,GAAa,KAClD+O,EAAUnP,EAAU4F,EAAI5F,EAAUuF,MAAQ,EAAItE,EAAQsE,MAAQ,EAC9D6J,EAAUpP,EAAU8F,EAAI9F,EAAUyF,OAAS,EAAIxE,EAAQwE,OAAS,EAGpE,OAAQgD,GACN,KAAK,EACH6B,EAAU,CACR1E,EAAGuJ,EACHrJ,EAAG9F,EAAU8F,EAAI7E,EAAQwE,QAE3B,MAEF,KAAKnG,EACHgL,EAAU,CACR1E,EAAGuJ,EACHrJ,EAAG9F,EAAU8F,EAAI9F,EAAUyF,QAE7B,MAEF,KAAKlG,EACH+K,EAAU,CACR1E,EAAG5F,EAAU4F,EAAI5F,EAAUuF,MAC3BO,EAAGsJ,GAEL,MAEF,KAAK5P,EACH8K,EAAU,CACR1E,EAAG5F,EAAU4F,EAAI3E,EAAQsE,MACzBO,EAAGsJ,GAEL,MAEF,QACE9E,EAAU,CACR1E,EAAG5F,EAAU4F,EACbE,EAAG9F,EAAU8F,GAInB,IAAIuJ,EAAW5G,EAAgBV,EAAyBU,GAAiB,KAEzE,GAAgB,MAAZ4G,EAAkB,CACpB,IAAI1G,EAAmB,MAAb0G,EAAmB,SAAW,QAExC,OAAQhF,GACN,KAAK1K,EACH2K,EAAQ+E,GAAY/E,EAAQ+E,IAAarP,EAAU2I,GAAO,EAAI1H,EAAQ0H,GAAO,GAC7E,MAEF,KAAK/I,EACH0K,EAAQ+E,GAAY/E,EAAQ+E,IAAarP,EAAU2I,GAAO,EAAI1H,EAAQ0H,GAAO,GAKnF,CAEA,OAAO2B,CACT,CC3De,SAASgF,GAAejN,EAAOc,QAC5B,IAAZA,IACFA,EAAU,CAAC,GAGb,IAAIoM,EAAWpM,EACXqM,EAAqBD,EAASnP,UAC9BA,OAAmC,IAAvBoP,EAAgCnN,EAAMjC,UAAYoP,EAC9DC,EAAoBF,EAASnM,SAC7BA,OAAiC,IAAtBqM,EAA+BpN,EAAMe,SAAWqM,EAC3DC,EAAoBH,EAASI,SAC7BA,OAAiC,IAAtBD,EAA+B7P,EAAkB6P,EAC5DE,EAAwBL,EAASM,aACjCA,OAAyC,IAA1BD,EAAmC9P,EAAW8P,EAC7DE,EAAwBP,EAASQ,eACjCA,OAA2C,IAA1BD,EAAmC/P,EAAS+P,EAC7DE,EAAuBT,EAASU,YAChCA,OAAuC,IAAzBD,GAA0CA,EACxDE,EAAmBX,EAAS3G,QAC5BA,OAA+B,IAArBsH,EAA8B,EAAIA,EAC5ChI,EAAgBD,EAAsC,iBAAZW,EAAuBA,EAAUT,EAAgBS,EAASlJ,IACpGyQ,EAAaJ,IAAmBhQ,EAASC,EAAYD,EACrDqK,EAAa/H,EAAMwG,MAAM9I,OACzBkB,EAAUoB,EAAME,SAAS0N,EAAcE,EAAaJ,GACpDK,EJkBS,SAAyBnP,EAAS0O,EAAUE,EAAczM,GACvE,IAAIiN,EAAmC,oBAAbV,EAlB5B,SAA4B1O,GAC1B,IAAIpB,EAAkBgO,GAAkB5G,EAAchG,IAElDqP,EADoB,CAAC,WAAY,SAASzJ,QAAQ,EAAiB5F,GAASiC,WAAa,GACnDtB,EAAcX,GAAWoG,EAAgBpG,GAAWA,EAE9F,OAAKS,EAAU4O,GAKRzQ,EAAgBgI,QAAO,SAAUyG,GACtC,OAAO5M,EAAU4M,IAAmBpI,EAASoI,EAAgBgC,IAAmD,SAAhCtP,EAAYsN,EAC9F,IANS,EAOX,CAK6DiC,CAAmBtP,GAAW,GAAGZ,OAAOsP,GAC/F9P,EAAkB,GAAGQ,OAAOgQ,EAAqB,CAACR,IAClDW,EAAsB3Q,EAAgB,GACtC4Q,EAAe5Q,EAAgBK,QAAO,SAAUwQ,EAASpC,GAC3D,IAAIF,EAAOC,GAA2BpN,EAASqN,EAAgBlL,GAK/D,OAJAsN,EAAQrN,IAAM,EAAI+K,EAAK/K,IAAKqN,EAAQrN,KACpCqN,EAAQnR,MAAQ,EAAI6O,EAAK7O,MAAOmR,EAAQnR,OACxCmR,EAAQpR,OAAS,EAAI8O,EAAK9O,OAAQoR,EAAQpR,QAC1CoR,EAAQlR,KAAO,EAAI4O,EAAK5O,KAAMkR,EAAQlR,MAC/BkR,CACT,GAAGrC,GAA2BpN,EAASuP,EAAqBpN,IAK5D,OAJAqN,EAAalL,MAAQkL,EAAalR,MAAQkR,EAAajR,KACvDiR,EAAahL,OAASgL,EAAanR,OAASmR,EAAapN,IACzDoN,EAAa7K,EAAI6K,EAAajR,KAC9BiR,EAAa3K,EAAI2K,EAAapN,IACvBoN,CACT,CInC2BE,CAAgBjP,EAAUT,GAAWA,EAAUA,EAAQ2P,gBAAkB9J,EAAmBzE,EAAME,SAASxC,QAAS4P,EAAUE,EAAczM,GACjKyN,EAAsB7L,EAAsB3C,EAAME,SAASvC,WAC3DuI,EAAgB2G,GAAe,CACjClP,UAAW6Q,EACX5P,QAASmJ,EACThH,SAAU,WACVhD,UAAWA,IAET0Q,EAAmB3C,GAAiBzP,OAAOkE,OAAO,CAAC,EAAGwH,EAAY7B,IAClEwI,EAAoBhB,IAAmBhQ,EAAS+Q,EAAmBD,EAGnEG,EAAkB,CACpB3N,IAAK+M,EAAmB/M,IAAM0N,EAAkB1N,IAAM6E,EAAc7E,IACpE/D,OAAQyR,EAAkBzR,OAAS8Q,EAAmB9Q,OAAS4I,EAAc5I,OAC7EE,KAAM4Q,EAAmB5Q,KAAOuR,EAAkBvR,KAAO0I,EAAc1I,KACvED,MAAOwR,EAAkBxR,MAAQ6Q,EAAmB7Q,MAAQ2I,EAAc3I,OAExE0R,EAAa5O,EAAMmG,cAAckB,OAErC,GAAIqG,IAAmBhQ,GAAUkR,EAAY,CAC3C,IAAIvH,EAASuH,EAAW7Q,GACxB1B,OAAO4D,KAAK0O,GAAiBxO,SAAQ,SAAUhE,GAC7C,IAAI0S,EAAW,CAAC3R,EAAOD,GAAQuH,QAAQrI,IAAQ,EAAI,GAAK,EACpDkK,EAAO,CAAC,EAAKpJ,GAAQuH,QAAQrI,IAAQ,EAAI,IAAM,IACnDwS,EAAgBxS,IAAQkL,EAAOhB,GAAQwI,CACzC,GACF,CAEA,OAAOF,CACT,CCyEA,UACEhP,KAAM,OACNC,SAAS,EACTC,MAAO,OACPC,GA5HF,SAAcC,GACZ,IAAIC,EAAQD,EAAKC,MACbc,EAAUf,EAAKe,QACfnB,EAAOI,EAAKJ,KAEhB,IAAIK,EAAMmG,cAAcxG,GAAMmP,MAA9B,CAoCA,IAhCA,IAAIC,EAAoBjO,EAAQkM,SAC5BgC,OAAsC,IAAtBD,GAAsCA,EACtDE,EAAmBnO,EAAQoO,QAC3BC,OAAoC,IAArBF,GAAqCA,EACpDG,EAA8BtO,EAAQuO,mBACtC9I,EAAUzF,EAAQyF,QAClB+G,EAAWxM,EAAQwM,SACnBE,EAAe1M,EAAQ0M,aACvBI,EAAc9M,EAAQ8M,YACtB0B,EAAwBxO,EAAQyO,eAChCA,OAA2C,IAA1BD,GAA0CA,EAC3DE,EAAwB1O,EAAQ0O,sBAChCC,EAAqBzP,EAAMc,QAAQ/C,UACnCqI,EAAgB9E,EAAiBmO,GAEjCJ,EAAqBD,IADHhJ,IAAkBqJ,GACqCF,EAjC/E,SAAuCxR,GACrC,GAAIuD,EAAiBvD,KAAeX,EAClC,MAAO,GAGT,IAAIsS,EAAoBnF,GAAqBxM,GAC7C,MAAO,CAAC2M,GAA8B3M,GAAY2R,EAAmBhF,GAA8BgF,GACrG,CA0B6IC,CAA8BF,GAA3E,CAAClF,GAAqBkF,KAChHG,EAAa,CAACH,GAAoBzR,OAAOqR,GAAoBxR,QAAO,SAAUC,EAAKC,GACrF,OAAOD,EAAIE,OAAOsD,EAAiBvD,KAAeX,ECvCvC,SAA8B4C,EAAOc,QAClC,IAAZA,IACFA,EAAU,CAAC,GAGb,IAAIoM,EAAWpM,EACX/C,EAAYmP,EAASnP,UACrBuP,EAAWJ,EAASI,SACpBE,EAAeN,EAASM,aACxBjH,EAAU2G,EAAS3G,QACnBgJ,EAAiBrC,EAASqC,eAC1BM,EAAwB3C,EAASsC,sBACjCA,OAAkD,IAA1BK,EAAmC,EAAgBA,EAC3E7H,EAAYL,EAAa5J,GACzB6R,EAAa5H,EAAYuH,EAAiB3R,EAAsBA,EAAoB4H,QAAO,SAAUzH,GACvG,OAAO4J,EAAa5J,KAAeiK,CACrC,IAAK3K,EACDyS,EAAoBF,EAAWpK,QAAO,SAAUzH,GAClD,OAAOyR,EAAsBhL,QAAQzG,IAAc,CACrD,IAEiC,IAA7B+R,EAAkBC,SACpBD,EAAoBF,GAItB,IAAII,EAAYF,EAAkBjS,QAAO,SAAUC,EAAKC,GAOtD,OANAD,EAAIC,GAAakP,GAAejN,EAAO,CACrCjC,UAAWA,EACXuP,SAAUA,EACVE,aAAcA,EACdjH,QAASA,IACRjF,EAAiBvD,IACbD,CACT,GAAG,CAAC,GACJ,OAAOzB,OAAO4D,KAAK+P,GAAWC,MAAK,SAAUC,EAAGC,GAC9C,OAAOH,EAAUE,GAAKF,EAAUG,EAClC,GACF,CDC6DC,CAAqBpQ,EAAO,CACnFjC,UAAWA,EACXuP,SAAUA,EACVE,aAAcA,EACdjH,QAASA,EACTgJ,eAAgBA,EAChBC,sBAAuBA,IACpBzR,EACP,GAAG,IACCsS,EAAgBrQ,EAAMwG,MAAM7I,UAC5BoK,EAAa/H,EAAMwG,MAAM9I,OACzB4S,EAAY,IAAIC,IAChBC,GAAqB,EACrBC,EAAwBb,EAAW,GAE9Bc,EAAI,EAAGA,EAAId,EAAWG,OAAQW,IAAK,CAC1C,IAAI3S,EAAY6R,EAAWc,GAEvBC,EAAiBrP,EAAiBvD,GAElC6S,EAAmBjJ,EAAa5J,KAAeT,EAC/CuT,EAAa,CAAC,EAAK5T,GAAQuH,QAAQmM,IAAmB,EACtDrK,EAAMuK,EAAa,QAAU,SAC7B1F,EAAW8B,GAAejN,EAAO,CACnCjC,UAAWA,EACXuP,SAAUA,EACVE,aAAcA,EACdI,YAAaA,EACbrH,QAASA,IAEPuK,EAAoBD,EAAaD,EAAmB1T,EAAQC,EAAOyT,EAAmB3T,EAAS,EAE/FoT,EAAc/J,GAAOyB,EAAWzB,KAClCwK,EAAoBvG,GAAqBuG,IAG3C,IAAIC,EAAmBxG,GAAqBuG,GACxCE,EAAS,GAUb,GARIhC,GACFgC,EAAOC,KAAK9F,EAASwF,IAAmB,GAGtCxB,GACF6B,EAAOC,KAAK9F,EAAS2F,IAAsB,EAAG3F,EAAS4F,IAAqB,GAG1EC,EAAOE,OAAM,SAAUC,GACzB,OAAOA,CACT,IAAI,CACFV,EAAwB1S,EACxByS,GAAqB,EACrB,KACF,CAEAF,EAAUc,IAAIrT,EAAWiT,EAC3B,CAEA,GAAIR,EAqBF,IAnBA,IAEIa,EAAQ,SAAeC,GACzB,IAAIC,EAAmB3B,EAAW4B,MAAK,SAAUzT,GAC/C,IAAIiT,EAASV,EAAU9T,IAAIuB,GAE3B,GAAIiT,EACF,OAAOA,EAAOS,MAAM,EAAGH,GAAIJ,OAAM,SAAUC,GACzC,OAAOA,CACT,GAEJ,IAEA,GAAII,EAEF,OADAd,EAAwBc,EACjB,OAEX,EAESD,EAnBY/B,EAAiB,EAAI,EAmBZ+B,EAAK,GAGpB,UAFFD,EAAMC,GADmBA,KAOpCtR,EAAMjC,YAAc0S,IACtBzQ,EAAMmG,cAAcxG,GAAMmP,OAAQ,EAClC9O,EAAMjC,UAAY0S,EAClBzQ,EAAM0R,OAAQ,EA5GhB,CA8GF,EAQEhK,iBAAkB,CAAC,UACnBgC,KAAM,CACJoF,OAAO,IE7IX,SAAS6C,GAAexG,EAAUY,EAAM6F,GAQtC,YAPyB,IAArBA,IACFA,EAAmB,CACjBrO,EAAG,EACHE,EAAG,IAIA,CACLzC,IAAKmK,EAASnK,IAAM+K,EAAK3I,OAASwO,EAAiBnO,EACnDvG,MAAOiO,EAASjO,MAAQ6O,EAAK7I,MAAQ0O,EAAiBrO,EACtDtG,OAAQkO,EAASlO,OAAS8O,EAAK3I,OAASwO,EAAiBnO,EACzDtG,KAAMgO,EAAShO,KAAO4O,EAAK7I,MAAQ0O,EAAiBrO,EAExD,CAEA,SAASsO,GAAsB1G,GAC7B,MAAO,CAAC,EAAKjO,EAAOD,EAAQE,GAAM2U,MAAK,SAAUC,GAC/C,OAAO5G,EAAS4G,IAAS,CAC3B,GACF,CA+BA,UACEpS,KAAM,OACNC,SAAS,EACTC,MAAO,OACP6H,iBAAkB,CAAC,mBACnB5H,GAlCF,SAAcC,GACZ,IAAIC,EAAQD,EAAKC,MACbL,EAAOI,EAAKJ,KACZ0Q,EAAgBrQ,EAAMwG,MAAM7I,UAC5BoK,EAAa/H,EAAMwG,MAAM9I,OACzBkU,EAAmB5R,EAAMmG,cAAc6L,gBACvCC,EAAoBhF,GAAejN,EAAO,CAC5C0N,eAAgB,cAEdwE,EAAoBjF,GAAejN,EAAO,CAC5C4N,aAAa,IAEXuE,EAA2BR,GAAeM,EAAmB5B,GAC7D+B,EAAsBT,GAAeO,EAAmBnK,EAAY6J,GACpES,EAAoBR,GAAsBM,GAC1CG,EAAmBT,GAAsBO,GAC7CpS,EAAMmG,cAAcxG,GAAQ,CAC1BwS,yBAA0BA,EAC1BC,oBAAqBA,EACrBC,kBAAmBA,EACnBC,iBAAkBA,GAEpBtS,EAAMM,WAAW5C,OAASrB,OAAOkE,OAAO,CAAC,EAAGP,EAAMM,WAAW5C,OAAQ,CACnE,+BAAgC2U,EAChC,sBAAuBC,GAE3B,GCJA,IACE3S,KAAM,SACNC,SAAS,EACTC,MAAO,OACPwB,SAAU,CAAC,iBACXvB,GA5BF,SAAgBa,GACd,IAAIX,EAAQW,EAAMX,MACdc,EAAUH,EAAMG,QAChBnB,EAAOgB,EAAMhB,KACb4S,EAAkBzR,EAAQuG,OAC1BA,OAA6B,IAApBkL,EAA6B,CAAC,EAAG,GAAKA,EAC/C7I,EAAO,EAAW7L,QAAO,SAAUC,EAAKC,GAE1C,OADAD,EAAIC,GA5BD,SAAiCA,EAAWyI,EAAOa,GACxD,IAAIjB,EAAgB9E,EAAiBvD,GACjCyU,EAAiB,CAACrV,EAAM,GAAKqH,QAAQ4B,IAAkB,GAAK,EAAI,EAEhErG,EAAyB,mBAAXsH,EAAwBA,EAAOhL,OAAOkE,OAAO,CAAC,EAAGiG,EAAO,CACxEzI,UAAWA,KACPsJ,EACFoL,EAAW1S,EAAK,GAChB2S,EAAW3S,EAAK,GAIpB,OAFA0S,EAAWA,GAAY,EACvBC,GAAYA,GAAY,GAAKF,EACtB,CAACrV,EAAMD,GAAOsH,QAAQ4B,IAAkB,EAAI,CACjD7C,EAAGmP,EACHjP,EAAGgP,GACD,CACFlP,EAAGkP,EACHhP,EAAGiP,EAEP,CASqBC,CAAwB5U,EAAWiC,EAAMwG,MAAOa,GAC1DvJ,CACT,GAAG,CAAC,GACA8U,EAAwBlJ,EAAK1J,EAAMjC,WACnCwF,EAAIqP,EAAsBrP,EAC1BE,EAAImP,EAAsBnP,EAEW,MAArCzD,EAAMmG,cAAcD,gBACtBlG,EAAMmG,cAAcD,cAAc3C,GAAKA,EACvCvD,EAAMmG,cAAcD,cAAczC,GAAKA,GAGzCzD,EAAMmG,cAAcxG,GAAQ+J,CAC9B,GC1BA,IACE/J,KAAM,gBACNC,SAAS,EACTC,MAAO,OACPC,GApBF,SAAuBC,GACrB,IAAIC,EAAQD,EAAKC,MACbL,EAAOI,EAAKJ,KAKhBK,EAAMmG,cAAcxG,GAAQkN,GAAe,CACzClP,UAAWqC,EAAMwG,MAAM7I,UACvBiB,QAASoB,EAAMwG,MAAM9I,OACrBqD,SAAU,WACVhD,UAAWiC,EAAMjC,WAErB,EAQE2L,KAAM,CAAC,GCgHT,IACE/J,KAAM,kBACNC,SAAS,EACTC,MAAO,OACPC,GA/HF,SAAyBC,GACvB,IAAIC,EAAQD,EAAKC,MACbc,EAAUf,EAAKe,QACfnB,EAAOI,EAAKJ,KACZoP,EAAoBjO,EAAQkM,SAC5BgC,OAAsC,IAAtBD,GAAsCA,EACtDE,EAAmBnO,EAAQoO,QAC3BC,OAAoC,IAArBF,GAAsCA,EACrD3B,EAAWxM,EAAQwM,SACnBE,EAAe1M,EAAQ0M,aACvBI,EAAc9M,EAAQ8M,YACtBrH,EAAUzF,EAAQyF,QAClBsM,EAAkB/R,EAAQgS,OAC1BA,OAA6B,IAApBD,GAAoCA,EAC7CE,EAAwBjS,EAAQkS,aAChCA,OAAyC,IAA1BD,EAAmC,EAAIA,EACtD5H,EAAW8B,GAAejN,EAAO,CACnCsN,SAAUA,EACVE,aAAcA,EACdjH,QAASA,EACTqH,YAAaA,IAEXxH,EAAgB9E,EAAiBtB,EAAMjC,WACvCiK,EAAYL,EAAa3H,EAAMjC,WAC/BkV,GAAmBjL,EACnBgF,EAAWtH,EAAyBU,GACpC8I,ECrCY,MDqCSlC,ECrCH,IAAM,IDsCxB9G,EAAgBlG,EAAMmG,cAAcD,cACpCmK,EAAgBrQ,EAAMwG,MAAM7I,UAC5BoK,EAAa/H,EAAMwG,MAAM9I,OACzBwV,EAA4C,mBAAjBF,EAA8BA,EAAa3W,OAAOkE,OAAO,CAAC,EAAGP,EAAMwG,MAAO,CACvGzI,UAAWiC,EAAMjC,aACbiV,EACFG,EAA2D,iBAAtBD,EAAiC,CACxElG,SAAUkG,EACVhE,QAASgE,GACP7W,OAAOkE,OAAO,CAChByM,SAAU,EACVkC,QAAS,GACRgE,GACCE,EAAsBpT,EAAMmG,cAAckB,OAASrH,EAAMmG,cAAckB,OAAOrH,EAAMjC,WAAa,KACjG2L,EAAO,CACTnG,EAAG,EACHE,EAAG,GAGL,GAAKyC,EAAL,CAIA,GAAI8I,EAAe,CACjB,IAAIqE,EAEAC,EAAwB,MAAbtG,EAAmB,EAAM7P,EACpCoW,EAAuB,MAAbvG,EAAmB/P,EAASC,EACtCoJ,EAAmB,MAAb0G,EAAmB,SAAW,QACpC3F,EAASnB,EAAc8G,GACvBtL,EAAM2F,EAAS8D,EAASmI,GACxB7R,EAAM4F,EAAS8D,EAASoI,GACxBC,EAAWV,GAAU/K,EAAWzB,GAAO,EAAI,EAC3CmN,EAASzL,IAAc1K,EAAQ+S,EAAc/J,GAAOyB,EAAWzB,GAC/DoN,EAAS1L,IAAc1K,GAASyK,EAAWzB,IAAQ+J,EAAc/J,GAGjEL,EAAejG,EAAME,SAASgB,MAC9BwF,EAAYoM,GAAU7M,EAAetC,EAAcsC,GAAgB,CACrE/C,MAAO,EACPE,OAAQ,GAENuQ,GAAqB3T,EAAMmG,cAAc,oBAAsBnG,EAAMmG,cAAc,oBAAoBI,QxBhFtG,CACLvF,IAAK,EACL9D,MAAO,EACPD,OAAQ,EACRE,KAAM,GwB6EFyW,GAAkBD,GAAmBL,GACrCO,GAAkBF,GAAmBJ,GAMrCO,GAAWnO,EAAO,EAAG0K,EAAc/J,GAAMI,EAAUJ,IACnDyN,GAAYd,EAAkB5C,EAAc/J,GAAO,EAAIkN,EAAWM,GAAWF,GAAkBT,EAA4BnG,SAAWyG,EAASK,GAAWF,GAAkBT,EAA4BnG,SACxMgH,GAAYf,GAAmB5C,EAAc/J,GAAO,EAAIkN,EAAWM,GAAWD,GAAkBV,EAA4BnG,SAAW0G,EAASI,GAAWD,GAAkBV,EAA4BnG,SACzMjG,GAAoB/G,EAAME,SAASgB,OAAS8D,EAAgBhF,EAAME,SAASgB,OAC3E+S,GAAelN,GAAiC,MAAbiG,EAAmBjG,GAAkBsF,WAAa,EAAItF,GAAkBuF,YAAc,EAAI,EAC7H4H,GAAwH,OAAjGb,EAA+C,MAAvBD,OAA8B,EAASA,EAAoBpG,IAAqBqG,EAAwB,EAEvJc,GAAY9M,EAAS2M,GAAYE,GACjCE,GAAkBzO,EAAOmN,EAAS,EAAQpR,EAF9B2F,EAAS0M,GAAYG,GAAsBD,IAEKvS,EAAK2F,EAAQyL,EAAS,EAAQrR,EAAK0S,IAAa1S,GAChHyE,EAAc8G,GAAYoH,GAC1B1K,EAAKsD,GAAYoH,GAAkB/M,CACrC,CAEA,GAAI8H,EAAc,CAChB,IAAIkF,GAEAC,GAAyB,MAAbtH,EAAmB,EAAM7P,EAErCoX,GAAwB,MAAbvH,EAAmB/P,EAASC,EAEvCsX,GAAUtO,EAAcgJ,GAExBuF,GAAmB,MAAZvF,EAAkB,SAAW,QAEpCwF,GAAOF,GAAUrJ,EAASmJ,IAE1BK,GAAOH,GAAUrJ,EAASoJ,IAE1BK,IAAuD,IAAxC,CAAC,EAAKzX,GAAMqH,QAAQ4B,GAEnCyO,GAAyH,OAAjGR,GAAgD,MAAvBjB,OAA8B,EAASA,EAAoBlE,IAAoBmF,GAAyB,EAEzJS,GAAaF,GAAeF,GAAOF,GAAUnE,EAAcoE,IAAQ1M,EAAW0M,IAAQI,GAAuB1B,EAA4BjE,QAEzI6F,GAAaH,GAAeJ,GAAUnE,EAAcoE,IAAQ1M,EAAW0M,IAAQI,GAAuB1B,EAA4BjE,QAAUyF,GAE5IK,GAAmBlC,GAAU8B,G1BzH9B,SAAwBlT,EAAK1E,EAAOyE,GACzC,IAAIwT,EAAItP,EAAOjE,EAAK1E,EAAOyE,GAC3B,OAAOwT,EAAIxT,EAAMA,EAAMwT,CACzB,C0BsHoDC,CAAeJ,GAAYN,GAASO,IAAcpP,EAAOmN,EAASgC,GAAaJ,GAAMF,GAAS1B,EAASiC,GAAaJ,IAEpKzO,EAAcgJ,GAAW8F,GACzBtL,EAAKwF,GAAW8F,GAAmBR,EACrC,CAEAxU,EAAMmG,cAAcxG,GAAQ+J,CAvE5B,CAwEF,EAQEhC,iBAAkB,CAAC,WE1HN,SAASyN,GAAiBC,EAAyBrQ,EAAcsD,QAC9D,IAAZA,IACFA,GAAU,GAGZ,ICnBoCrJ,ECJOJ,EFuBvCyW,EAA0B9V,EAAcwF,GACxCuQ,EAAuB/V,EAAcwF,IAf3C,SAAyBnG,GACvB,IAAImN,EAAOnN,EAAQ+D,wBACfI,EAASpB,EAAMoK,EAAK7I,OAAStE,EAAQqE,aAAe,EACpDD,EAASrB,EAAMoK,EAAK3I,QAAUxE,EAAQuE,cAAgB,EAC1D,OAAkB,IAAXJ,GAA2B,IAAXC,CACzB,CAU4DuS,CAAgBxQ,GACtEJ,EAAkBF,EAAmBM,GACrCgH,EAAOpJ,EAAsByS,EAAyBE,EAAsBjN,GAC5EyB,EAAS,CACXc,WAAY,EACZE,UAAW,GAET7C,EAAU,CACZ1E,EAAG,EACHE,EAAG,GAkBL,OAfI4R,IAA4BA,IAA4BhN,MACxB,SAA9B1J,EAAYoG,IAChBkG,GAAetG,MACbmF,GCnCgC9K,EDmCT+F,KClCdhG,EAAUC,IAAUO,EAAcP,GCJxC,CACL4L,YAFyChM,EDQbI,GCNR4L,WACpBE,UAAWlM,EAAQkM,WDGZH,GAAgB3L,IDoCnBO,EAAcwF,KAChBkD,EAAUtF,EAAsBoC,GAAc,IACtCxB,GAAKwB,EAAauH,WAC1BrE,EAAQxE,GAAKsB,EAAasH,WACjB1H,IACTsD,EAAQ1E,EAAIyH,GAAoBrG,KAI7B,CACLpB,EAAGwI,EAAK5O,KAAO2M,EAAOc,WAAa3C,EAAQ1E,EAC3CE,EAAGsI,EAAK/K,IAAM8I,EAAOgB,UAAY7C,EAAQxE,EACzCP,MAAO6I,EAAK7I,MACZE,OAAQ2I,EAAK3I,OAEjB,CGvDA,SAASoS,GAAMC,GACb,IAAItT,EAAM,IAAIoO,IACVmF,EAAU,IAAIC,IACdC,EAAS,GAKb,SAAS3F,EAAK4F,GACZH,EAAQI,IAAID,EAASlW,MACN,GAAG3B,OAAO6X,EAASxU,UAAY,GAAIwU,EAASnO,kBAAoB,IACtEvH,SAAQ,SAAU4V,GACzB,IAAKL,EAAQM,IAAID,GAAM,CACrB,IAAIE,EAAc9T,EAAI3F,IAAIuZ,GAEtBE,GACFhG,EAAKgG,EAET,CACF,IACAL,EAAO3E,KAAK4E,EACd,CAQA,OAzBAJ,EAAUtV,SAAQ,SAAU0V,GAC1B1T,EAAIiP,IAAIyE,EAASlW,KAAMkW,EACzB,IAiBAJ,EAAUtV,SAAQ,SAAU0V,GACrBH,EAAQM,IAAIH,EAASlW,OAExBsQ,EAAK4F,EAET,IACOD,CACT,CCvBA,IAAIM,GAAkB,CACpBnY,UAAW,SACX0X,UAAW,GACX1U,SAAU,YAGZ,SAASoV,KACP,IAAK,IAAI1B,EAAO2B,UAAUrG,OAAQsG,EAAO,IAAIpU,MAAMwS,GAAO6B,EAAO,EAAGA,EAAO7B,EAAM6B,IAC/ED,EAAKC,GAAQF,UAAUE,GAGzB,OAAQD,EAAKvE,MAAK,SAAUlT,GAC1B,QAASA,GAAoD,mBAAlCA,EAAQ+D,sBACrC,GACF,CAEO,SAAS4T,GAAgBC,QACL,IAArBA,IACFA,EAAmB,CAAC,GAGtB,IAAIC,EAAoBD,EACpBE,EAAwBD,EAAkBE,iBAC1CA,OAA6C,IAA1BD,EAAmC,GAAKA,EAC3DE,EAAyBH,EAAkBI,eAC3CA,OAA4C,IAA3BD,EAAoCV,GAAkBU,EAC3E,OAAO,SAAsBjZ,EAAWD,EAAQoD,QAC9B,IAAZA,IACFA,EAAU+V,GAGZ,ICxC6B/W,EAC3BgX,EDuCE9W,EAAQ,CACVjC,UAAW,SACXgZ,iBAAkB,GAClBjW,QAASzE,OAAOkE,OAAO,CAAC,EAAG2V,GAAiBW,GAC5C1Q,cAAe,CAAC,EAChBjG,SAAU,CACRvC,UAAWA,EACXD,OAAQA,GAEV4C,WAAY,CAAC,EACbD,OAAQ,CAAC,GAEP2W,EAAmB,GACnBC,GAAc,EACdrN,EAAW,CACb5J,MAAOA,EACPkX,WAAY,SAAoBC,GAC9B,IAAIrW,EAAsC,mBAArBqW,EAAkCA,EAAiBnX,EAAMc,SAAWqW,EACzFC,IACApX,EAAMc,QAAUzE,OAAOkE,OAAO,CAAC,EAAGsW,EAAgB7W,EAAMc,QAASA,GACjEd,EAAMiK,cAAgB,CACpBtM,UAAW0B,EAAU1B,GAAa6N,GAAkB7N,GAAaA,EAAU4Q,eAAiB/C,GAAkB7N,EAAU4Q,gBAAkB,GAC1I7Q,OAAQ8N,GAAkB9N,IAI5B,IElE4B+X,EAC9B4B,EFiEMN,EDhCG,SAAwBtB,GAErC,IAAIsB,EAAmBvB,GAAMC,GAE7B,OAAO/W,EAAeb,QAAO,SAAUC,EAAK+B,GAC1C,OAAO/B,EAAIE,OAAO+Y,EAAiBvR,QAAO,SAAUqQ,GAClD,OAAOA,EAAShW,QAAUA,CAC5B,IACF,GAAG,GACL,CCuB+ByX,EElEK7B,EFkEsB,GAAGzX,OAAO2Y,EAAkB3W,EAAMc,QAAQ2U,WEjE9F4B,EAAS5B,EAAU5X,QAAO,SAAUwZ,EAAQE,GAC9C,IAAIC,EAAWH,EAAOE,EAAQ5X,MAK9B,OAJA0X,EAAOE,EAAQ5X,MAAQ6X,EAAWnb,OAAOkE,OAAO,CAAC,EAAGiX,EAAUD,EAAS,CACrEzW,QAASzE,OAAOkE,OAAO,CAAC,EAAGiX,EAAS1W,QAASyW,EAAQzW,SACrD4I,KAAMrN,OAAOkE,OAAO,CAAC,EAAGiX,EAAS9N,KAAM6N,EAAQ7N,QAC5C6N,EACEF,CACT,GAAG,CAAC,GAEGhb,OAAO4D,KAAKoX,GAAQlV,KAAI,SAAUhG,GACvC,OAAOkb,EAAOlb,EAChB,MF4DM,OAJA6D,EAAM+W,iBAAmBA,EAAiBvR,QAAO,SAAUiS,GACzD,OAAOA,EAAE7X,OACX,IA+FFI,EAAM+W,iBAAiB5W,SAAQ,SAAUJ,GACvC,IAAIJ,EAAOI,EAAKJ,KACZ+X,EAAe3X,EAAKe,QACpBA,OAA2B,IAAjB4W,EAA0B,CAAC,EAAIA,EACzChX,EAASX,EAAKW,OAElB,GAAsB,mBAAXA,EAAuB,CAChC,IAAIiX,EAAYjX,EAAO,CACrBV,MAAOA,EACPL,KAAMA,EACNiK,SAAUA,EACV9I,QAASA,IAKXkW,EAAiB/F,KAAK0G,GAFT,WAAmB,EAGlC,CACF,IA/GS/N,EAASQ,QAClB,EAMAwN,YAAa,WACX,IAAIX,EAAJ,CAIA,IAAIY,EAAkB7X,EAAME,SACxBvC,EAAYka,EAAgBla,UAC5BD,EAASma,EAAgBna,OAG7B,GAAKyY,GAAiBxY,EAAWD,GAAjC,CAKAsC,EAAMwG,MAAQ,CACZ7I,UAAWwX,GAAiBxX,EAAWqH,EAAgBtH,GAAoC,UAA3BsC,EAAMc,QAAQC,UAC9ErD,OAAQiG,EAAcjG,IAOxBsC,EAAM0R,OAAQ,EACd1R,EAAMjC,UAAYiC,EAAMc,QAAQ/C,UAKhCiC,EAAM+W,iBAAiB5W,SAAQ,SAAU0V,GACvC,OAAO7V,EAAMmG,cAAc0P,EAASlW,MAAQtD,OAAOkE,OAAO,CAAC,EAAGsV,EAASnM,KACzE,IAEA,IAAK,IAAIoO,EAAQ,EAAGA,EAAQ9X,EAAM+W,iBAAiBhH,OAAQ+H,IACzD,IAAoB,IAAhB9X,EAAM0R,MAAV,CAMA,IAAIqG,EAAwB/X,EAAM+W,iBAAiBe,GAC/ChY,EAAKiY,EAAsBjY,GAC3BkY,EAAyBD,EAAsBjX,QAC/CoM,OAAsC,IAA3B8K,EAAoC,CAAC,EAAIA,EACpDrY,EAAOoY,EAAsBpY,KAEf,mBAAPG,IACTE,EAAQF,EAAG,CACTE,MAAOA,EACPc,QAASoM,EACTvN,KAAMA,EACNiK,SAAUA,KACN5J,EAdR,MAHEA,EAAM0R,OAAQ,EACdoG,GAAS,CAzBb,CATA,CAqDF,EAGA1N,QC1I2BtK,ED0IV,WACf,OAAO,IAAImY,SAAQ,SAAUC,GAC3BtO,EAASgO,cACTM,EAAQlY,EACV,GACF,EC7IG,WAUL,OATK8W,IACHA,EAAU,IAAImB,SAAQ,SAAUC,GAC9BD,QAAQC,UAAUC,MAAK,WACrBrB,OAAUsB,EACVF,EAAQpY,IACV,GACF,KAGKgX,CACT,GDmIIuB,QAAS,WACPjB,IACAH,GAAc,CAChB,GAGF,IAAKd,GAAiBxY,EAAWD,GAC/B,OAAOkM,EAmCT,SAASwN,IACPJ,EAAiB7W,SAAQ,SAAUL,GACjC,OAAOA,GACT,IACAkX,EAAmB,EACrB,CAEA,OAvCApN,EAASsN,WAAWpW,GAASqX,MAAK,SAAUnY,IACrCiX,GAAenW,EAAQwX,eAC1BxX,EAAQwX,cAActY,EAE1B,IAmCO4J,CACT,CACF,CACO,IAAI2O,GAA4BhC,KGzLnC,GAA4BA,GAAgB,CAC9CI,iBAFqB,CAAC6B,GAAgB,GAAe,GAAe,EAAa,GAAQ,GAAM,GAAiB,EAAO,MCJrH,GAA4BjC,GAAgB,CAC9CI,iBAFqB,CAAC6B,GAAgB,GAAe,GAAe,KCatE,MAAMC,GAAa,IAAIlI,IACjBmI,GAAO,CACX,GAAAtH,CAAIxS,EAASzC,EAAKyN,GACX6O,GAAWzC,IAAIpX,IAClB6Z,GAAWrH,IAAIxS,EAAS,IAAI2R,KAE9B,MAAMoI,EAAcF,GAAWjc,IAAIoC,GAI9B+Z,EAAY3C,IAAI7Z,IAA6B,IAArBwc,EAAYC,KAKzCD,EAAYvH,IAAIjV,EAAKyN,GAHnBiP,QAAQC,MAAM,+EAA+E7W,MAAM8W,KAAKJ,EAAY1Y,QAAQ,MAIhI,EACAzD,IAAG,CAACoC,EAASzC,IACPsc,GAAWzC,IAAIpX,IACV6Z,GAAWjc,IAAIoC,GAASpC,IAAIL,IAE9B,KAET,MAAA6c,CAAOpa,EAASzC,GACd,IAAKsc,GAAWzC,IAAIpX,GAClB,OAEF,MAAM+Z,EAAcF,GAAWjc,IAAIoC,GACnC+Z,EAAYM,OAAO9c,GAGM,IAArBwc,EAAYC,MACdH,GAAWQ,OAAOra,EAEtB,GAYIsa,GAAiB,gBAOjBC,GAAgBC,IAChBA,GAAYna,OAAOoa,KAAOpa,OAAOoa,IAAIC,SAEvCF,EAAWA,EAAS5O,QAAQ,iBAAiB,CAAC+O,EAAOC,IAAO,IAAIH,IAAIC,OAAOE,QAEtEJ,GA4CHK,GAAuB7a,IAC3BA,EAAQ8a,cAAc,IAAIC,MAAMT,IAAgB,EAE5C,GAAYU,MACXA,GAA4B,iBAAXA,UAGO,IAAlBA,EAAOC,SAChBD,EAASA,EAAO,SAEgB,IAApBA,EAAOE,UAEjBC,GAAaH,GAEb,GAAUA,GACLA,EAAOC,OAASD,EAAO,GAAKA,EAEf,iBAAXA,GAAuBA,EAAO7J,OAAS,EACzCrL,SAAS+C,cAAc0R,GAAcS,IAEvC,KAEHI,GAAYpb,IAChB,IAAK,GAAUA,IAAgD,IAApCA,EAAQqb,iBAAiBlK,OAClD,OAAO,EAET,MAAMmK,EAAgF,YAA7D5V,iBAAiB1F,GAASub,iBAAiB,cAE9DC,EAAgBxb,EAAQyb,QAAQ,uBACtC,IAAKD,EACH,OAAOF,EAET,GAAIE,IAAkBxb,EAAS,CAC7B,MAAM0b,EAAU1b,EAAQyb,QAAQ,WAChC,GAAIC,GAAWA,EAAQlW,aAAegW,EACpC,OAAO,EAET,GAAgB,OAAZE,EACF,OAAO,CAEX,CACA,OAAOJ,CAAgB,EAEnBK,GAAa3b,IACZA,GAAWA,EAAQkb,WAAaU,KAAKC,gBAGtC7b,EAAQ8b,UAAU7W,SAAS,mBAGC,IAArBjF,EAAQ+b,SACV/b,EAAQ+b,SAEV/b,EAAQgc,aAAa,aAAoD,UAArChc,EAAQic,aAAa,aAE5DC,GAAiBlc,IACrB,IAAK8F,SAASC,gBAAgBoW,aAC5B,OAAO,KAIT,GAAmC,mBAAxBnc,EAAQqF,YAA4B,CAC7C,MAAM+W,EAAOpc,EAAQqF,cACrB,OAAO+W,aAAgBtb,WAAasb,EAAO,IAC7C,CACA,OAAIpc,aAAmBc,WACdd,EAIJA,EAAQwF,WAGN0W,GAAelc,EAAQwF,YAFrB,IAEgC,EAErC6W,GAAO,OAUPC,GAAStc,IACbA,EAAQuE,YAAY,EAGhBgY,GAAY,IACZlc,OAAOmc,SAAW1W,SAAS6G,KAAKqP,aAAa,qBACxC3b,OAAOmc,OAET,KAEHC,GAA4B,GAgB5BC,GAAQ,IAAuC,QAAjC5W,SAASC,gBAAgB4W,IACvCC,GAAqBC,IAhBAC,QAiBN,KACjB,MAAMC,EAAIR,KAEV,GAAIQ,EAAG,CACL,MAAMhc,EAAO8b,EAAOG,KACdC,EAAqBF,EAAE7b,GAAGH,GAChCgc,EAAE7b,GAAGH,GAAQ8b,EAAOK,gBACpBH,EAAE7b,GAAGH,GAAMoc,YAAcN,EACzBE,EAAE7b,GAAGH,GAAMqc,WAAa,KACtBL,EAAE7b,GAAGH,GAAQkc,EACNJ,EAAOK,gBAElB,GA5B0B,YAAxBpX,SAASuX,YAENZ,GAA0BtL,QAC7BrL,SAASyF,iBAAiB,oBAAoB,KAC5C,IAAK,MAAMuR,KAAYL,GACrBK,GACF,IAGJL,GAA0BpK,KAAKyK,IAE/BA,GAkBA,EAEEQ,GAAU,CAACC,EAAkB9F,EAAO,GAAI+F,EAAeD,IACxB,mBAArBA,EAAkCA,KAAoB9F,GAAQ+F,EAExEC,GAAyB,CAACX,EAAUY,EAAmBC,GAAoB,KAC/E,IAAKA,EAEH,YADAL,GAAQR,GAGV,MACMc,EAhKiC5d,KACvC,IAAKA,EACH,OAAO,EAIT,IAAI,mBACF6d,EAAkB,gBAClBC,GACEzd,OAAOqF,iBAAiB1F,GAC5B,MAAM+d,EAA0BC,OAAOC,WAAWJ,GAC5CK,EAAuBF,OAAOC,WAAWH,GAG/C,OAAKC,GAA4BG,GAKjCL,EAAqBA,EAAmBlb,MAAM,KAAK,GACnDmb,EAAkBA,EAAgBnb,MAAM,KAAK,GAtDf,KAuDtBqb,OAAOC,WAAWJ,GAAsBG,OAAOC,WAAWH,KANzD,CAMoG,EA2IpFK,CAAiCT,GADlC,EAExB,IAAIU,GAAS,EACb,MAAMC,EAAU,EACdrR,aAEIA,IAAW0Q,IAGfU,GAAS,EACTV,EAAkBjS,oBAAoB6O,GAAgB+D,GACtDf,GAAQR,GAAS,EAEnBY,EAAkBnS,iBAAiB+O,GAAgB+D,GACnDC,YAAW,KACJF,GACHvD,GAAqB6C,EACvB,GACCE,EAAiB,EAYhBW,GAAuB,CAAC1R,EAAM2R,EAAeC,EAAeC,KAChE,MAAMC,EAAa9R,EAAKsE,OACxB,IAAI+H,EAAQrM,EAAKjH,QAAQ4Y,GAIzB,OAAe,IAAXtF,GACMuF,GAAiBC,EAAiB7R,EAAK8R,EAAa,GAAK9R,EAAK,IAExEqM,GAASuF,EAAgB,GAAK,EAC1BC,IACFxF,GAASA,EAAQyF,GAAcA,GAE1B9R,EAAKjK,KAAKC,IAAI,EAAGD,KAAKE,IAAIoW,EAAOyF,EAAa,KAAI,EAerDC,GAAiB,qBACjBC,GAAiB,OACjBC,GAAgB,SAChBC,GAAgB,CAAC,EACvB,IAAIC,GAAW,EACf,MAAMC,GAAe,CACnBC,WAAY,YACZC,WAAY,YAERC,GAAe,IAAIrI,IAAI,CAAC,QAAS,WAAY,UAAW,YAAa,cAAe,aAAc,iBAAkB,YAAa,WAAY,YAAa,cAAe,YAAa,UAAW,WAAY,QAAS,oBAAqB,aAAc,YAAa,WAAY,cAAe,cAAe,cAAe,YAAa,eAAgB,gBAAiB,eAAgB,gBAAiB,aAAc,QAAS,OAAQ,SAAU,QAAS,SAAU,SAAU,UAAW,WAAY,OAAQ,SAAU,eAAgB,SAAU,OAAQ,mBAAoB,mBAAoB,QAAS,QAAS,WAM/lB,SAASsI,GAAarf,EAASsf,GAC7B,OAAOA,GAAO,GAAGA,MAAQN,QAAgBhf,EAAQgf,UAAYA,IAC/D,CACA,SAASO,GAAiBvf,GACxB,MAAMsf,EAAMD,GAAarf,GAGzB,OAFAA,EAAQgf,SAAWM,EACnBP,GAAcO,GAAOP,GAAcO,IAAQ,CAAC,EACrCP,GAAcO,EACvB,CAiCA,SAASE,GAAYC,EAAQC,EAAUC,EAAqB,MAC1D,OAAOliB,OAAOmiB,OAAOH,GAAQ7M,MAAKiN,GAASA,EAAMH,WAAaA,GAAYG,EAAMF,qBAAuBA,GACzG,CACA,SAASG,GAAoBC,EAAmB1B,EAAS2B,GACvD,MAAMC,EAAiC,iBAAZ5B,EAErBqB,EAAWO,EAAcD,EAAqB3B,GAAW2B,EAC/D,IAAIE,EAAYC,GAAaJ,GAI7B,OAHKX,GAAahI,IAAI8I,KACpBA,EAAYH,GAEP,CAACE,EAAaP,EAAUQ,EACjC,CACA,SAASE,GAAWpgB,EAAS+f,EAAmB1B,EAAS2B,EAAoBK,GAC3E,GAAiC,iBAAtBN,IAAmC/f,EAC5C,OAEF,IAAKigB,EAAaP,EAAUQ,GAAaJ,GAAoBC,EAAmB1B,EAAS2B,GAIzF,GAAID,KAAqBd,GAAc,CACrC,MAAMqB,EAAepf,GACZ,SAAU2e,GACf,IAAKA,EAAMU,eAAiBV,EAAMU,gBAAkBV,EAAMW,iBAAmBX,EAAMW,eAAevb,SAAS4a,EAAMU,eAC/G,OAAOrf,EAAGjD,KAAKwiB,KAAMZ,EAEzB,EAEFH,EAAWY,EAAaZ,EAC1B,CACA,MAAMD,EAASF,GAAiBvf,GAC1B0gB,EAAWjB,EAAOS,KAAeT,EAAOS,GAAa,CAAC,GACtDS,EAAmBnB,GAAYkB,EAAUhB,EAAUO,EAAc5B,EAAU,MACjF,GAAIsC,EAEF,YADAA,EAAiBN,OAASM,EAAiBN,QAAUA,GAGvD,MAAMf,EAAMD,GAAaK,EAAUK,EAAkBnU,QAAQgT,GAAgB,KACvE1d,EAAK+e,EA5Db,SAAoCjgB,EAASwa,EAAUtZ,GACrD,OAAO,SAASmd,EAAQwB,GACtB,MAAMe,EAAc5gB,EAAQ6gB,iBAAiBrG,GAC7C,IAAK,IAAI,OACPxN,GACE6S,EAAO7S,GAAUA,IAAWyT,KAAMzT,EAASA,EAAOxH,WACpD,IAAK,MAAMsb,KAAcF,EACvB,GAAIE,IAAe9T,EASnB,OANA+T,GAAWlB,EAAO,CAChBW,eAAgBxT,IAEdqR,EAAQgC,QACVW,GAAaC,IAAIjhB,EAAS6f,EAAMqB,KAAM1G,EAAUtZ,GAE3CA,EAAGigB,MAAMnU,EAAQ,CAAC6S,GAG/B,CACF,CAwC2BuB,CAA2BphB,EAASqe,EAASqB,GAvExE,SAA0B1f,EAASkB,GACjC,OAAO,SAASmd,EAAQwB,GAOtB,OANAkB,GAAWlB,EAAO,CAChBW,eAAgBxgB,IAEdqe,EAAQgC,QACVW,GAAaC,IAAIjhB,EAAS6f,EAAMqB,KAAMhgB,GAEjCA,EAAGigB,MAAMnhB,EAAS,CAAC6f,GAC5B,CACF,CA6DoFwB,CAAiBrhB,EAAS0f,GAC5Gxe,EAAGye,mBAAqBM,EAAc5B,EAAU,KAChDnd,EAAGwe,SAAWA,EACdxe,EAAGmf,OAASA,EACZnf,EAAG8d,SAAWM,EACdoB,EAASpB,GAAOpe,EAChBlB,EAAQuL,iBAAiB2U,EAAWhf,EAAI+e,EAC1C,CACA,SAASqB,GAActhB,EAASyf,EAAQS,EAAW7B,EAASsB,GAC1D,MAAMze,EAAKse,GAAYC,EAAOS,GAAY7B,EAASsB,GAC9Cze,IAGLlB,EAAQyL,oBAAoByU,EAAWhf,EAAIqgB,QAAQ5B,WAC5CF,EAAOS,GAAWhf,EAAG8d,UAC9B,CACA,SAASwC,GAAyBxhB,EAASyf,EAAQS,EAAWuB,GAC5D,MAAMC,EAAoBjC,EAAOS,IAAc,CAAC,EAChD,IAAK,MAAOyB,EAAY9B,KAAUpiB,OAAOmkB,QAAQF,GAC3CC,EAAWE,SAASJ,IACtBH,GAActhB,EAASyf,EAAQS,EAAWL,EAAMH,SAAUG,EAAMF,mBAGtE,CACA,SAASQ,GAAaN,GAGpB,OADAA,EAAQA,EAAMjU,QAAQiT,GAAgB,IAC/BI,GAAaY,IAAUA,CAChC,CACA,MAAMmB,GAAe,CACnB,EAAAc,CAAG9hB,EAAS6f,EAAOxB,EAAS2B,GAC1BI,GAAWpgB,EAAS6f,EAAOxB,EAAS2B,GAAoB,EAC1D,EACA,GAAA+B,CAAI/hB,EAAS6f,EAAOxB,EAAS2B,GAC3BI,GAAWpgB,EAAS6f,EAAOxB,EAAS2B,GAAoB,EAC1D,EACA,GAAAiB,CAAIjhB,EAAS+f,EAAmB1B,EAAS2B,GACvC,GAAiC,iBAAtBD,IAAmC/f,EAC5C,OAEF,MAAOigB,EAAaP,EAAUQ,GAAaJ,GAAoBC,EAAmB1B,EAAS2B,GACrFgC,EAAc9B,IAAcH,EAC5BN,EAASF,GAAiBvf,GAC1B0hB,EAAoBjC,EAAOS,IAAc,CAAC,EAC1C+B,EAAclC,EAAkBmC,WAAW,KACjD,QAAwB,IAAbxC,EAAX,CAQA,GAAIuC,EACF,IAAK,MAAME,KAAgB1kB,OAAO4D,KAAKoe,GACrC+B,GAAyBxhB,EAASyf,EAAQ0C,EAAcpC,EAAkBlN,MAAM,IAGpF,IAAK,MAAOuP,EAAavC,KAAUpiB,OAAOmkB,QAAQF,GAAoB,CACpE,MAAMC,EAAaS,EAAYxW,QAAQkT,GAAe,IACjDkD,IAAejC,EAAkB8B,SAASF,IAC7CL,GAActhB,EAASyf,EAAQS,EAAWL,EAAMH,SAAUG,EAAMF,mBAEpE,CAXA,KAPA,CAEE,IAAKliB,OAAO4D,KAAKqgB,GAAmBvQ,OAClC,OAEFmQ,GAActhB,EAASyf,EAAQS,EAAWR,EAAUO,EAAc5B,EAAU,KAE9E,CAYF,EACA,OAAAgE,CAAQriB,EAAS6f,EAAOpI,GACtB,GAAqB,iBAAVoI,IAAuB7f,EAChC,OAAO,KAET,MAAM+c,EAAIR,KAGV,IAAI+F,EAAc,KACdC,GAAU,EACVC,GAAiB,EACjBC,GAAmB,EAJH5C,IADFM,GAAaN,IAMZ9C,IACjBuF,EAAcvF,EAAEhC,MAAM8E,EAAOpI,GAC7BsF,EAAE/c,GAASqiB,QAAQC,GACnBC,GAAWD,EAAYI,uBACvBF,GAAkBF,EAAYK,gCAC9BF,EAAmBH,EAAYM,sBAEjC,MAAMC,EAAM9B,GAAW,IAAIhG,MAAM8E,EAAO,CACtC0C,UACAO,YAAY,IACVrL,GAUJ,OATIgL,GACFI,EAAIE,iBAEFP,GACFxiB,EAAQ8a,cAAc+H,GAEpBA,EAAIJ,kBAAoBH,GAC1BA,EAAYS,iBAEPF,CACT,GAEF,SAAS9B,GAAWljB,EAAKmlB,EAAO,CAAC,GAC/B,IAAK,MAAOzlB,EAAKa,KAAUX,OAAOmkB,QAAQoB,GACxC,IACEnlB,EAAIN,GAAOa,CACb,CAAE,MAAO6kB,GACPxlB,OAAOC,eAAeG,EAAKN,EAAK,CAC9B2lB,cAAc,EACdtlB,IAAG,IACMQ,GAGb,CAEF,OAAOP,CACT,CASA,SAASslB,GAAc/kB,GACrB,GAAc,SAAVA,EACF,OAAO,EAET,GAAc,UAAVA,EACF,OAAO,EAET,GAAIA,IAAU4f,OAAO5f,GAAOkC,WAC1B,OAAO0d,OAAO5f,GAEhB,GAAc,KAAVA,GAA0B,SAAVA,EAClB,OAAO,KAET,GAAqB,iBAAVA,EACT,OAAOA,EAET,IACE,OAAOglB,KAAKC,MAAMC,mBAAmBllB,GACvC,CAAE,MAAO6kB,GACP,OAAO7kB,CACT,CACF,CACA,SAASmlB,GAAiBhmB,GACxB,OAAOA,EAAIqO,QAAQ,UAAU4X,GAAO,IAAIA,EAAItjB,iBAC9C,CACA,MAAMujB,GAAc,CAClB,gBAAAC,CAAiB1jB,EAASzC,EAAKa,GAC7B4B,EAAQ6B,aAAa,WAAW0hB,GAAiBhmB,KAAQa,EAC3D,EACA,mBAAAulB,CAAoB3jB,EAASzC,GAC3ByC,EAAQ4B,gBAAgB,WAAW2hB,GAAiBhmB,KACtD,EACA,iBAAAqmB,CAAkB5jB,GAChB,IAAKA,EACH,MAAO,CAAC,EAEV,MAAM0B,EAAa,CAAC,EACdmiB,EAASpmB,OAAO4D,KAAKrB,EAAQ8jB,SAASld,QAAOrJ,GAAOA,EAAI2kB,WAAW,QAAU3kB,EAAI2kB,WAAW,cAClG,IAAK,MAAM3kB,KAAOsmB,EAAQ,CACxB,IAAIE,EAAUxmB,EAAIqO,QAAQ,MAAO,IACjCmY,EAAUA,EAAQC,OAAO,GAAG9jB,cAAgB6jB,EAAQlR,MAAM,EAAGkR,EAAQ5S,QACrEzP,EAAWqiB,GAAWZ,GAAcnjB,EAAQ8jB,QAAQvmB,GACtD,CACA,OAAOmE,CACT,EACAuiB,iBAAgB,CAACjkB,EAASzC,IACjB4lB,GAAcnjB,EAAQic,aAAa,WAAWsH,GAAiBhmB,QAgB1E,MAAM2mB,GAEJ,kBAAWC,GACT,MAAO,CAAC,CACV,CACA,sBAAWC,GACT,MAAO,CAAC,CACV,CACA,eAAWpH,GACT,MAAM,IAAIqH,MAAM,sEAClB,CACA,UAAAC,CAAWC,GAIT,OAHAA,EAAS9D,KAAK+D,gBAAgBD,GAC9BA,EAAS9D,KAAKgE,kBAAkBF,GAChC9D,KAAKiE,iBAAiBH,GACfA,CACT,CACA,iBAAAE,CAAkBF,GAChB,OAAOA,CACT,CACA,eAAAC,CAAgBD,EAAQvkB,GACtB,MAAM2kB,EAAa,GAAU3kB,GAAWyjB,GAAYQ,iBAAiBjkB,EAAS,UAAY,CAAC,EAE3F,MAAO,IACFygB,KAAKmE,YAAYT,WACM,iBAAfQ,EAA0BA,EAAa,CAAC,KAC/C,GAAU3kB,GAAWyjB,GAAYG,kBAAkB5jB,GAAW,CAAC,KAC7C,iBAAXukB,EAAsBA,EAAS,CAAC,EAE/C,CACA,gBAAAG,CAAiBH,EAAQM,EAAcpE,KAAKmE,YAAYR,aACtD,IAAK,MAAO7hB,EAAUuiB,KAAkBrnB,OAAOmkB,QAAQiD,GAAc,CACnE,MAAMzmB,EAAQmmB,EAAOhiB,GACfwiB,EAAY,GAAU3mB,GAAS,UAjiBrC4c,OADSA,EAkiB+C5c,GAhiBnD,GAAG4c,IAELvd,OAAOM,UAAUuC,SAASrC,KAAK+c,GAAQL,MAAM,eAAe,GAAGza,cA+hBlE,IAAK,IAAI8kB,OAAOF,GAAehhB,KAAKihB,GAClC,MAAM,IAAIE,UAAU,GAAGxE,KAAKmE,YAAY5H,KAAKkI,0BAA0B3iB,qBAA4BwiB,yBAAiCD,MAExI,CAtiBW9J,KAuiBb,EAqBF,MAAMmK,WAAsBjB,GAC1B,WAAAU,CAAY5kB,EAASukB,GACnBa,SACAplB,EAAUmb,GAAWnb,MAIrBygB,KAAK4E,SAAWrlB,EAChBygB,KAAK6E,QAAU7E,KAAK6D,WAAWC,GAC/BzK,GAAKtH,IAAIiO,KAAK4E,SAAU5E,KAAKmE,YAAYW,SAAU9E,MACrD,CAGA,OAAA+E,GACE1L,GAAKM,OAAOqG,KAAK4E,SAAU5E,KAAKmE,YAAYW,UAC5CvE,GAAaC,IAAIR,KAAK4E,SAAU5E,KAAKmE,YAAYa,WACjD,IAAK,MAAMC,KAAgBjoB,OAAOkoB,oBAAoBlF,MACpDA,KAAKiF,GAAgB,IAEzB,CACA,cAAAE,CAAe9I,EAAU9c,EAAS6lB,GAAa,GAC7CpI,GAAuBX,EAAU9c,EAAS6lB,EAC5C,CACA,UAAAvB,CAAWC,GAIT,OAHAA,EAAS9D,KAAK+D,gBAAgBD,EAAQ9D,KAAK4E,UAC3Cd,EAAS9D,KAAKgE,kBAAkBF,GAChC9D,KAAKiE,iBAAiBH,GACfA,CACT,CAGA,kBAAOuB,CAAY9lB,GACjB,OAAO8Z,GAAKlc,IAAIud,GAAWnb,GAAUygB,KAAK8E,SAC5C,CACA,0BAAOQ,CAAoB/lB,EAASukB,EAAS,CAAC,GAC5C,OAAO9D,KAAKqF,YAAY9lB,IAAY,IAAIygB,KAAKzgB,EAA2B,iBAAXukB,EAAsBA,EAAS,KAC9F,CACA,kBAAWyB,GACT,MA5CY,OA6Cd,CACA,mBAAWT,GACT,MAAO,MAAM9E,KAAKzD,MACpB,CACA,oBAAWyI,GACT,MAAO,IAAIhF,KAAK8E,UAClB,CACA,gBAAOU,CAAUllB,GACf,MAAO,GAAGA,IAAO0f,KAAKgF,WACxB,EAUF,MAAMS,GAAclmB,IAClB,IAAIwa,EAAWxa,EAAQic,aAAa,kBACpC,IAAKzB,GAAyB,MAAbA,EAAkB,CACjC,IAAI2L,EAAgBnmB,EAAQic,aAAa,QAMzC,IAAKkK,IAAkBA,EAActE,SAAS,OAASsE,EAAcjE,WAAW,KAC9E,OAAO,KAILiE,EAActE,SAAS,OAASsE,EAAcjE,WAAW,OAC3DiE,EAAgB,IAAIA,EAAcxjB,MAAM,KAAK,MAE/C6X,EAAW2L,GAAmC,MAAlBA,EAAwB5L,GAAc4L,EAAcC,QAAU,IAC5F,CACA,OAAO5L,CAAQ,EAEX6L,GAAiB,CACrBzT,KAAI,CAAC4H,EAAUxa,EAAU8F,SAASC,kBACzB,GAAG3G,UAAUsB,QAAQ3C,UAAU8iB,iBAAiB5iB,KAAK+B,EAASwa,IAEvE8L,QAAO,CAAC9L,EAAUxa,EAAU8F,SAASC,kBAC5BrF,QAAQ3C,UAAU8K,cAAc5K,KAAK+B,EAASwa,GAEvD+L,SAAQ,CAACvmB,EAASwa,IACT,GAAGpb,UAAUY,EAAQumB,UAAU3f,QAAOzB,GAASA,EAAMqhB,QAAQhM,KAEtE,OAAAiM,CAAQzmB,EAASwa,GACf,MAAMiM,EAAU,GAChB,IAAIC,EAAW1mB,EAAQwF,WAAWiW,QAAQjB,GAC1C,KAAOkM,GACLD,EAAQpU,KAAKqU,GACbA,EAAWA,EAASlhB,WAAWiW,QAAQjB,GAEzC,OAAOiM,CACT,EACA,IAAAE,CAAK3mB,EAASwa,GACZ,IAAIoM,EAAW5mB,EAAQ6mB,uBACvB,KAAOD,GAAU,CACf,GAAIA,EAASJ,QAAQhM,GACnB,MAAO,CAACoM,GAEVA,EAAWA,EAASC,sBACtB,CACA,MAAO,EACT,EAEA,IAAAvhB,CAAKtF,EAASwa,GACZ,IAAIlV,EAAOtF,EAAQ8mB,mBACnB,KAAOxhB,GAAM,CACX,GAAIA,EAAKkhB,QAAQhM,GACf,MAAO,CAAClV,GAEVA,EAAOA,EAAKwhB,kBACd,CACA,MAAO,EACT,EACA,iBAAAC,CAAkB/mB,GAChB,MAAMgnB,EAAa,CAAC,IAAK,SAAU,QAAS,WAAY,SAAU,UAAW,aAAc,4BAA4BzjB,KAAIiX,GAAY,GAAGA,2BAAiC7W,KAAK,KAChL,OAAO8c,KAAK7N,KAAKoU,EAAYhnB,GAAS4G,QAAOqgB,IAAOtL,GAAWsL,IAAO7L,GAAU6L,IAClF,EACA,sBAAAC,CAAuBlnB,GACrB,MAAMwa,EAAW0L,GAAYlmB,GAC7B,OAAIwa,GACK6L,GAAeC,QAAQ9L,GAAYA,EAErC,IACT,EACA,sBAAA2M,CAAuBnnB,GACrB,MAAMwa,EAAW0L,GAAYlmB,GAC7B,OAAOwa,EAAW6L,GAAeC,QAAQ9L,GAAY,IACvD,EACA,+BAAA4M,CAAgCpnB,GAC9B,MAAMwa,EAAW0L,GAAYlmB,GAC7B,OAAOwa,EAAW6L,GAAezT,KAAK4H,GAAY,EACpD,GAUI6M,GAAuB,CAACC,EAAWC,EAAS,UAChD,MAAMC,EAAa,gBAAgBF,EAAU7B,YACvC1kB,EAAOumB,EAAUtK,KACvBgE,GAAac,GAAGhc,SAAU0hB,EAAY,qBAAqBzmB,OAAU,SAAU8e,GAI7E,GAHI,CAAC,IAAK,QAAQgC,SAASpB,KAAKgH,UAC9B5H,EAAMkD,iBAEJpH,GAAW8E,MACb,OAEF,MAAMzT,EAASqZ,GAAec,uBAAuB1G,OAASA,KAAKhF,QAAQ,IAAI1a,KAC9DumB,EAAUvB,oBAAoB/Y,GAGtCua,IACX,GAAE,EAiBEG,GAAc,YACdC,GAAc,QAAQD,KACtBE,GAAe,SAASF,KAQ9B,MAAMG,WAAc1C,GAElB,eAAWnI,GACT,MAfW,OAgBb,CAGA,KAAA8K,GAEE,GADmB9G,GAAaqB,QAAQ5B,KAAK4E,SAAUsC,IACxClF,iBACb,OAEFhC,KAAK4E,SAASvJ,UAAU1B,OAlBF,QAmBtB,MAAMyL,EAAapF,KAAK4E,SAASvJ,UAAU7W,SApBrB,QAqBtBwb,KAAKmF,gBAAe,IAAMnF,KAAKsH,mBAAmBtH,KAAK4E,SAAUQ,EACnE,CAGA,eAAAkC,GACEtH,KAAK4E,SAASjL,SACd4G,GAAaqB,QAAQ5B,KAAK4E,SAAUuC,IACpCnH,KAAK+E,SACP,CAGA,sBAAOtI,CAAgBqH,GACrB,OAAO9D,KAAKuH,MAAK,WACf,MAAMld,EAAO+c,GAAM9B,oBAAoBtF,MACvC,GAAsB,iBAAX8D,EAAX,CAGA,QAAqB/K,IAAjB1O,EAAKyZ,IAAyBA,EAAOrC,WAAW,MAAmB,gBAAXqC,EAC1D,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,GAAQ9D,KAJb,CAKF,GACF,EAOF4G,GAAqBQ,GAAO,SAM5BjL,GAAmBiL,IAcnB,MAKMI,GAAyB,4BAO/B,MAAMC,WAAe/C,GAEnB,eAAWnI,GACT,MAfW,QAgBb,CAGA,MAAAmL,GAEE1H,KAAK4E,SAASxjB,aAAa,eAAgB4e,KAAK4E,SAASvJ,UAAUqM,OAjB3C,UAkB1B,CAGA,sBAAOjL,CAAgBqH,GACrB,OAAO9D,KAAKuH,MAAK,WACf,MAAMld,EAAOod,GAAOnC,oBAAoBtF,MACzB,WAAX8D,GACFzZ,EAAKyZ,IAET,GACF,EAOFvD,GAAac,GAAGhc,SAjCe,2BAiCmBmiB,IAAwBpI,IACxEA,EAAMkD,iBACN,MAAMqF,EAASvI,EAAM7S,OAAOyO,QAAQwM,IACvBC,GAAOnC,oBAAoBqC,GACnCD,QAAQ,IAOfvL,GAAmBsL,IAcnB,MACMG,GAAc,YACdC,GAAmB,aAAaD,KAChCE,GAAkB,YAAYF,KAC9BG,GAAiB,WAAWH,KAC5BI,GAAoB,cAAcJ,KAClCK,GAAkB,YAAYL,KAK9BM,GAAY,CAChBC,YAAa,KACbC,aAAc,KACdC,cAAe,MAEXC,GAAgB,CACpBH,YAAa,kBACbC,aAAc,kBACdC,cAAe,mBAOjB,MAAME,WAAc9E,GAClB,WAAAU,CAAY5kB,EAASukB,GACnBa,QACA3E,KAAK4E,SAAWrlB,EACXA,GAAYgpB,GAAMC,gBAGvBxI,KAAK6E,QAAU7E,KAAK6D,WAAWC,GAC/B9D,KAAKyI,QAAU,EACfzI,KAAK0I,sBAAwB5H,QAAQlhB,OAAO+oB,cAC5C3I,KAAK4I,cACP,CAGA,kBAAWlF,GACT,OAAOwE,EACT,CACA,sBAAWvE,GACT,OAAO2E,EACT,CACA,eAAW/L,GACT,MA/CW,OAgDb,CAGA,OAAAwI,GACExE,GAAaC,IAAIR,KAAK4E,SAAUgD,GAClC,CAGA,MAAAiB,CAAOzJ,GACAY,KAAK0I,sBAIN1I,KAAK8I,wBAAwB1J,KAC/BY,KAAKyI,QAAUrJ,EAAM2J,SAJrB/I,KAAKyI,QAAUrJ,EAAM4J,QAAQ,GAAGD,OAMpC,CACA,IAAAE,CAAK7J,GACCY,KAAK8I,wBAAwB1J,KAC/BY,KAAKyI,QAAUrJ,EAAM2J,QAAU/I,KAAKyI,SAEtCzI,KAAKkJ,eACLrM,GAAQmD,KAAK6E,QAAQsD,YACvB,CACA,KAAAgB,CAAM/J,GACJY,KAAKyI,QAAUrJ,EAAM4J,SAAW5J,EAAM4J,QAAQtY,OAAS,EAAI,EAAI0O,EAAM4J,QAAQ,GAAGD,QAAU/I,KAAKyI,OACjG,CACA,YAAAS,GACE,MAAME,EAAYjnB,KAAKoC,IAAIyb,KAAKyI,SAChC,GAAIW,GAnEgB,GAoElB,OAEF,MAAM9b,EAAY8b,EAAYpJ,KAAKyI,QACnCzI,KAAKyI,QAAU,EACVnb,GAGLuP,GAAQvP,EAAY,EAAI0S,KAAK6E,QAAQwD,cAAgBrI,KAAK6E,QAAQuD,aACpE,CACA,WAAAQ,GACM5I,KAAK0I,uBACPnI,GAAac,GAAGrB,KAAK4E,SAAUoD,IAAmB5I,GAASY,KAAK6I,OAAOzJ,KACvEmB,GAAac,GAAGrB,KAAK4E,SAAUqD,IAAiB7I,GAASY,KAAKiJ,KAAK7J,KACnEY,KAAK4E,SAASvJ,UAAU5E,IAlFG,mBAoF3B8J,GAAac,GAAGrB,KAAK4E,SAAUiD,IAAkBzI,GAASY,KAAK6I,OAAOzJ,KACtEmB,GAAac,GAAGrB,KAAK4E,SAAUkD,IAAiB1I,GAASY,KAAKmJ,MAAM/J,KACpEmB,GAAac,GAAGrB,KAAK4E,SAAUmD,IAAgB3I,GAASY,KAAKiJ,KAAK7J,KAEtE,CACA,uBAAA0J,CAAwB1J,GACtB,OAAOY,KAAK0I,wBA3FS,QA2FiBtJ,EAAMiK,aA5FrB,UA4FyDjK,EAAMiK,YACxF,CAGA,kBAAOb,GACL,MAAO,iBAAkBnjB,SAASC,iBAAmB7C,UAAU6mB,eAAiB,CAClF,EAeF,MAEMC,GAAc,eACdC,GAAiB,YAKjBC,GAAa,OACbC,GAAa,OACbC,GAAiB,OACjBC,GAAkB,QAClBC,GAAc,QAAQN,KACtBO,GAAa,OAAOP,KACpBQ,GAAkB,UAAUR,KAC5BS,GAAqB,aAAaT,KAClCU,GAAqB,aAAaV,KAClCW,GAAmB,YAAYX,KAC/BY,GAAwB,OAAOZ,KAAcC,KAC7CY,GAAyB,QAAQb,KAAcC,KAC/Ca,GAAsB,WACtBC,GAAsB,SAMtBC,GAAkB,UAClBC,GAAgB,iBAChBC,GAAuBF,GAAkBC,GAKzCE,GAAmB,CACvB,UAAoBd,GACpB,WAAqBD,IAEjBgB,GAAY,CAChBC,SAAU,IACVC,UAAU,EACVC,MAAO,QACPC,MAAM,EACNC,OAAO,EACPC,MAAM,GAEFC,GAAgB,CACpBN,SAAU,mBAEVC,SAAU,UACVC,MAAO,mBACPC,KAAM,mBACNC,MAAO,UACPC,KAAM,WAOR,MAAME,WAAiBzG,GACrB,WAAAP,CAAY5kB,EAASukB,GACnBa,MAAMplB,EAASukB,GACf9D,KAAKoL,UAAY,KACjBpL,KAAKqL,eAAiB,KACtBrL,KAAKsL,YAAa,EAClBtL,KAAKuL,aAAe,KACpBvL,KAAKwL,aAAe,KACpBxL,KAAKyL,mBAAqB7F,GAAeC,QArCjB,uBAqC8C7F,KAAK4E,UAC3E5E,KAAK0L,qBACD1L,KAAK6E,QAAQkG,OAASV,IACxBrK,KAAK2L,OAET,CAGA,kBAAWjI,GACT,OAAOiH,EACT,CACA,sBAAWhH,GACT,OAAOuH,EACT,CACA,eAAW3O,GACT,MAnFW,UAoFb,CAGA,IAAA1X,GACEmb,KAAK4L,OAAOnC,GACd,CACA,eAAAoC,IAIOxmB,SAASymB,QAAUnR,GAAUqF,KAAK4E,WACrC5E,KAAKnb,MAET,CACA,IAAAqhB,GACElG,KAAK4L,OAAOlC,GACd,CACA,KAAAoB,GACM9K,KAAKsL,YACPlR,GAAqB4F,KAAK4E,UAE5B5E,KAAK+L,gBACP,CACA,KAAAJ,GACE3L,KAAK+L,iBACL/L,KAAKgM,kBACLhM,KAAKoL,UAAYa,aAAY,IAAMjM,KAAK6L,mBAAmB7L,KAAK6E,QAAQ+F,SAC1E,CACA,iBAAAsB,GACOlM,KAAK6E,QAAQkG,OAGd/K,KAAKsL,WACP/K,GAAae,IAAItB,KAAK4E,SAAUkF,IAAY,IAAM9J,KAAK2L,UAGzD3L,KAAK2L,QACP,CACA,EAAAQ,CAAG1T,GACD,MAAM2T,EAAQpM,KAAKqM,YACnB,GAAI5T,EAAQ2T,EAAM1b,OAAS,GAAK+H,EAAQ,EACtC,OAEF,GAAIuH,KAAKsL,WAEP,YADA/K,GAAae,IAAItB,KAAK4E,SAAUkF,IAAY,IAAM9J,KAAKmM,GAAG1T,KAG5D,MAAM6T,EAActM,KAAKuM,cAAcvM,KAAKwM,cAC5C,GAAIF,IAAgB7T,EAClB,OAEF,MAAMtC,EAAQsC,EAAQ6T,EAAc7C,GAAaC,GACjD1J,KAAK4L,OAAOzV,EAAOiW,EAAM3T,GAC3B,CACA,OAAAsM,GACM/E,KAAKwL,cACPxL,KAAKwL,aAAazG,UAEpBJ,MAAMI,SACR,CAGA,iBAAAf,CAAkBF,GAEhB,OADAA,EAAO2I,gBAAkB3I,EAAO8G,SACzB9G,CACT,CACA,kBAAA4H,GACM1L,KAAK6E,QAAQgG,UACftK,GAAac,GAAGrB,KAAK4E,SAAUmF,IAAiB3K,GAASY,KAAK0M,SAAStN,KAE9C,UAAvBY,KAAK6E,QAAQiG,QACfvK,GAAac,GAAGrB,KAAK4E,SAAUoF,IAAoB,IAAMhK,KAAK8K,UAC9DvK,GAAac,GAAGrB,KAAK4E,SAAUqF,IAAoB,IAAMjK,KAAKkM,uBAE5DlM,KAAK6E,QAAQmG,OAASzC,GAAMC,eAC9BxI,KAAK2M,yBAET,CACA,uBAAAA,GACE,IAAK,MAAMC,KAAOhH,GAAezT,KArIX,qBAqImC6N,KAAK4E,UAC5DrE,GAAac,GAAGuL,EAAK1C,IAAkB9K,GAASA,EAAMkD,mBAExD,MAmBMuK,EAAc,CAClBzE,aAAc,IAAMpI,KAAK4L,OAAO5L,KAAK8M,kBAAkBnD,KACvDtB,cAAe,IAAMrI,KAAK4L,OAAO5L,KAAK8M,kBAAkBlD,KACxDzB,YAtBkB,KACS,UAAvBnI,KAAK6E,QAAQiG,QAYjB9K,KAAK8K,QACD9K,KAAKuL,cACPwB,aAAa/M,KAAKuL,cAEpBvL,KAAKuL,aAAe1N,YAAW,IAAMmC,KAAKkM,qBAjLjB,IAiL+DlM,KAAK6E,QAAQ+F,UAAS,GAOhH5K,KAAKwL,aAAe,IAAIjD,GAAMvI,KAAK4E,SAAUiI,EAC/C,CACA,QAAAH,CAAStN,GACP,GAAI,kBAAkB/b,KAAK+b,EAAM7S,OAAOya,SACtC,OAEF,MAAM1Z,EAAYod,GAAiBtL,EAAMtiB,KACrCwQ,IACF8R,EAAMkD,iBACNtC,KAAK4L,OAAO5L,KAAK8M,kBAAkBxf,IAEvC,CACA,aAAAif,CAAchtB,GACZ,OAAOygB,KAAKqM,YAAYlnB,QAAQ5F,EAClC,CACA,0BAAAytB,CAA2BvU,GACzB,IAAKuH,KAAKyL,mBACR,OAEF,MAAMwB,EAAkBrH,GAAeC,QAAQ0E,GAAiBvK,KAAKyL,oBACrEwB,EAAgB5R,UAAU1B,OAAO2Q,IACjC2C,EAAgB9rB,gBAAgB,gBAChC,MAAM+rB,EAAqBtH,GAAeC,QAAQ,sBAAsBpN,MAAWuH,KAAKyL,oBACpFyB,IACFA,EAAmB7R,UAAU5E,IAAI6T,IACjC4C,EAAmB9rB,aAAa,eAAgB,QAEpD,CACA,eAAA4qB,GACE,MAAMzsB,EAAUygB,KAAKqL,gBAAkBrL,KAAKwM,aAC5C,IAAKjtB,EACH,OAEF,MAAM4tB,EAAkB5P,OAAO6P,SAAS7tB,EAAQic,aAAa,oBAAqB,IAClFwE,KAAK6E,QAAQ+F,SAAWuC,GAAmBnN,KAAK6E,QAAQ4H,eAC1D,CACA,MAAAb,CAAOzV,EAAO5W,EAAU,MACtB,GAAIygB,KAAKsL,WACP,OAEF,MAAMvN,EAAgBiC,KAAKwM,aACrBa,EAASlX,IAAUsT,GACnB6D,EAAc/tB,GAAWue,GAAqBkC,KAAKqM,YAAatO,EAAesP,EAAQrN,KAAK6E,QAAQoG,MAC1G,GAAIqC,IAAgBvP,EAClB,OAEF,MAAMwP,EAAmBvN,KAAKuM,cAAce,GACtCE,EAAehI,GACZjF,GAAaqB,QAAQ5B,KAAK4E,SAAUY,EAAW,CACpD1F,cAAewN,EACfhgB,UAAW0S,KAAKyN,kBAAkBtX,GAClCuD,KAAMsG,KAAKuM,cAAcxO,GACzBoO,GAAIoB,IAIR,GADmBC,EAAa3D,IACjB7H,iBACb,OAEF,IAAKjE,IAAkBuP,EAGrB,OAEF,MAAMI,EAAY5M,QAAQd,KAAKoL,WAC/BpL,KAAK8K,QACL9K,KAAKsL,YAAa,EAClBtL,KAAKgN,2BAA2BO,GAChCvN,KAAKqL,eAAiBiC,EACtB,MAAMK,EAAuBN,EA3OR,sBADF,oBA6ObO,EAAiBP,EA3OH,qBACA,qBA2OpBC,EAAYjS,UAAU5E,IAAImX,GAC1B/R,GAAOyR,GACPvP,EAAc1C,UAAU5E,IAAIkX,GAC5BL,EAAYjS,UAAU5E,IAAIkX,GAQ1B3N,KAAKmF,gBAPoB,KACvBmI,EAAYjS,UAAU1B,OAAOgU,EAAsBC,GACnDN,EAAYjS,UAAU5E,IAAI6T,IAC1BvM,EAAc1C,UAAU1B,OAAO2Q,GAAqBsD,EAAgBD,GACpE3N,KAAKsL,YAAa,EAClBkC,EAAa1D,GAAW,GAEY/L,EAAeiC,KAAK6N,eACtDH,GACF1N,KAAK2L,OAET,CACA,WAAAkC,GACE,OAAO7N,KAAK4E,SAASvJ,UAAU7W,SAhQV,QAiQvB,CACA,UAAAgoB,GACE,OAAO5G,GAAeC,QAAQ4E,GAAsBzK,KAAK4E,SAC3D,CACA,SAAAyH,GACE,OAAOzG,GAAezT,KAAKqY,GAAexK,KAAK4E,SACjD,CACA,cAAAmH,GACM/L,KAAKoL,YACP0C,cAAc9N,KAAKoL,WACnBpL,KAAKoL,UAAY,KAErB,CACA,iBAAA0B,CAAkBxf,GAChB,OAAI2O,KACK3O,IAAcqc,GAAiBD,GAAaD,GAE9Cnc,IAAcqc,GAAiBF,GAAaC,EACrD,CACA,iBAAA+D,CAAkBtX,GAChB,OAAI8F,KACK9F,IAAUuT,GAAaC,GAAiBC,GAE1CzT,IAAUuT,GAAaE,GAAkBD,EAClD,CAGA,sBAAOlN,CAAgBqH,GACrB,OAAO9D,KAAKuH,MAAK,WACf,MAAMld,EAAO8gB,GAAS7F,oBAAoBtF,KAAM8D,GAChD,GAAsB,iBAAXA,GAIX,GAAsB,iBAAXA,EAAqB,CAC9B,QAAqB/K,IAAjB1O,EAAKyZ,IAAyBA,EAAOrC,WAAW,MAAmB,gBAAXqC,EAC1D,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,IACP,OAREzZ,EAAK8hB,GAAGrI,EASZ,GACF,EAOFvD,GAAac,GAAGhc,SAAU+kB,GAvSE,uCAuS2C,SAAUhL,GAC/E,MAAM7S,EAASqZ,GAAec,uBAAuB1G,MACrD,IAAKzT,IAAWA,EAAO8O,UAAU7W,SAAS6lB,IACxC,OAEFjL,EAAMkD,iBACN,MAAMyL,EAAW5C,GAAS7F,oBAAoB/Y,GACxCyhB,EAAahO,KAAKxE,aAAa,oBACrC,OAAIwS,GACFD,EAAS5B,GAAG6B,QACZD,EAAS7B,qBAGyC,SAAhDlJ,GAAYQ,iBAAiBxD,KAAM,UACrC+N,EAASlpB,YACTkpB,EAAS7B,sBAGX6B,EAAS7H,YACT6H,EAAS7B,oBACX,IACA3L,GAAac,GAAGzhB,OAAQuqB,IAAuB,KAC7C,MAAM8D,EAAYrI,GAAezT,KA5TR,6BA6TzB,IAAK,MAAM4b,KAAYE,EACrB9C,GAAS7F,oBAAoByI,EAC/B,IAOF5R,GAAmBgP,IAcnB,MAEM+C,GAAc,eAEdC,GAAe,OAAOD,KACtBE,GAAgB,QAAQF,KACxBG,GAAe,OAAOH,KACtBI,GAAiB,SAASJ,KAC1BK,GAAyB,QAAQL,cACjCM,GAAoB,OACpBC,GAAsB,WACtBC,GAAwB,aAExBC,GAA6B,WAAWF,OAAwBA,KAKhEG,GAAyB,8BACzBC,GAAY,CAChBpqB,OAAQ,KACRijB,QAAQ,GAEJoH,GAAgB,CACpBrqB,OAAQ,iBACRijB,OAAQ,WAOV,MAAMqH,WAAiBrK,GACrB,WAAAP,CAAY5kB,EAASukB,GACnBa,MAAMplB,EAASukB,GACf9D,KAAKgP,kBAAmB,EACxBhP,KAAKiP,cAAgB,GACrB,MAAMC,EAAatJ,GAAezT,KAAKyc,IACvC,IAAK,MAAMO,KAAQD,EAAY,CAC7B,MAAMnV,EAAW6L,GAAea,uBAAuB0I,GACjDC,EAAgBxJ,GAAezT,KAAK4H,GAAU5T,QAAOkpB,GAAgBA,IAAiBrP,KAAK4E,WAChF,OAAb7K,GAAqBqV,EAAc1e,QACrCsP,KAAKiP,cAAcrd,KAAKud,EAE5B,CACAnP,KAAKsP,sBACAtP,KAAK6E,QAAQpgB,QAChBub,KAAKuP,0BAA0BvP,KAAKiP,cAAejP,KAAKwP,YAEtDxP,KAAK6E,QAAQ6C,QACf1H,KAAK0H,QAET,CAGA,kBAAWhE,GACT,OAAOmL,EACT,CACA,sBAAWlL,GACT,OAAOmL,EACT,CACA,eAAWvS,GACT,MA9DW,UA+Db,CAGA,MAAAmL,GACM1H,KAAKwP,WACPxP,KAAKyP,OAELzP,KAAK0P,MAET,CACA,IAAAA,GACE,GAAI1P,KAAKgP,kBAAoBhP,KAAKwP,WAChC,OAEF,IAAIG,EAAiB,GAQrB,GALI3P,KAAK6E,QAAQpgB,SACfkrB,EAAiB3P,KAAK4P,uBAhEH,wCAgE4CzpB,QAAO5G,GAAWA,IAAYygB,KAAK4E,WAAU9hB,KAAIvD,GAAWwvB,GAASzJ,oBAAoB/lB,EAAS,CAC/JmoB,QAAQ,OAGRiI,EAAejf,QAAUif,EAAe,GAAGX,iBAC7C,OAGF,GADmBzO,GAAaqB,QAAQ5B,KAAK4E,SAAUuJ,IACxCnM,iBACb,OAEF,IAAK,MAAM6N,KAAkBF,EAC3BE,EAAeJ,OAEjB,MAAMK,EAAY9P,KAAK+P,gBACvB/P,KAAK4E,SAASvJ,UAAU1B,OAAO8U,IAC/BzO,KAAK4E,SAASvJ,UAAU5E,IAAIiY,IAC5B1O,KAAK4E,SAAS7jB,MAAM+uB,GAAa,EACjC9P,KAAKuP,0BAA0BvP,KAAKiP,eAAe,GACnDjP,KAAKgP,kBAAmB,EACxB,MAQMgB,EAAa,SADUF,EAAU,GAAGrL,cAAgBqL,EAAU1d,MAAM,KAE1E4N,KAAKmF,gBATY,KACfnF,KAAKgP,kBAAmB,EACxBhP,KAAK4E,SAASvJ,UAAU1B,OAAO+U,IAC/B1O,KAAK4E,SAASvJ,UAAU5E,IAAIgY,GAAqBD,IACjDxO,KAAK4E,SAAS7jB,MAAM+uB,GAAa,GACjCvP,GAAaqB,QAAQ5B,KAAK4E,SAAUwJ,GAAc,GAItBpO,KAAK4E,UAAU,GAC7C5E,KAAK4E,SAAS7jB,MAAM+uB,GAAa,GAAG9P,KAAK4E,SAASoL,MACpD,CACA,IAAAP,GACE,GAAIzP,KAAKgP,mBAAqBhP,KAAKwP,WACjC,OAGF,GADmBjP,GAAaqB,QAAQ5B,KAAK4E,SAAUyJ,IACxCrM,iBACb,OAEF,MAAM8N,EAAY9P,KAAK+P,gBACvB/P,KAAK4E,SAAS7jB,MAAM+uB,GAAa,GAAG9P,KAAK4E,SAASthB,wBAAwBwsB,OAC1EjU,GAAOmE,KAAK4E,UACZ5E,KAAK4E,SAASvJ,UAAU5E,IAAIiY,IAC5B1O,KAAK4E,SAASvJ,UAAU1B,OAAO8U,GAAqBD,IACpD,IAAK,MAAM5M,KAAW5B,KAAKiP,cAAe,CACxC,MAAM1vB,EAAUqmB,GAAec,uBAAuB9E,GAClDriB,IAAYygB,KAAKwP,SAASjwB,IAC5BygB,KAAKuP,0BAA0B,CAAC3N,IAAU,EAE9C,CACA5B,KAAKgP,kBAAmB,EAOxBhP,KAAK4E,SAAS7jB,MAAM+uB,GAAa,GACjC9P,KAAKmF,gBAPY,KACfnF,KAAKgP,kBAAmB,EACxBhP,KAAK4E,SAASvJ,UAAU1B,OAAO+U,IAC/B1O,KAAK4E,SAASvJ,UAAU5E,IAAIgY,IAC5BlO,GAAaqB,QAAQ5B,KAAK4E,SAAU0J,GAAe,GAGvBtO,KAAK4E,UAAU,EAC/C,CACA,QAAA4K,CAASjwB,EAAUygB,KAAK4E,UACtB,OAAOrlB,EAAQ8b,UAAU7W,SAASgqB,GACpC,CAGA,iBAAAxK,CAAkBF,GAGhB,OAFAA,EAAO4D,OAAS5G,QAAQgD,EAAO4D,QAC/B5D,EAAOrf,OAASiW,GAAWoJ,EAAOrf,QAC3Bqf,CACT,CACA,aAAAiM,GACE,OAAO/P,KAAK4E,SAASvJ,UAAU7W,SA3IL,uBAChB,QACC,QA0Ib,CACA,mBAAA8qB,GACE,IAAKtP,KAAK6E,QAAQpgB,OAChB,OAEF,MAAMqhB,EAAW9F,KAAK4P,uBAAuBhB,IAC7C,IAAK,MAAMrvB,KAAWumB,EAAU,CAC9B,MAAMmK,EAAWrK,GAAec,uBAAuBnnB,GACnD0wB,GACFjQ,KAAKuP,0BAA0B,CAAChwB,GAAUygB,KAAKwP,SAASS,GAE5D,CACF,CACA,sBAAAL,CAAuB7V,GACrB,MAAM+L,EAAWF,GAAezT,KAAKwc,GAA4B3O,KAAK6E,QAAQpgB,QAE9E,OAAOmhB,GAAezT,KAAK4H,EAAUiG,KAAK6E,QAAQpgB,QAAQ0B,QAAO5G,IAAYumB,EAAS1E,SAAS7hB,IACjG,CACA,yBAAAgwB,CAA0BW,EAAcC,GACtC,GAAKD,EAAaxf,OAGlB,IAAK,MAAMnR,KAAW2wB,EACpB3wB,EAAQ8b,UAAUqM,OArKK,aAqKyByI,GAChD5wB,EAAQ6B,aAAa,gBAAiB+uB,EAE1C,CAGA,sBAAO1T,CAAgBqH,GACrB,MAAMe,EAAU,CAAC,EAIjB,MAHsB,iBAAXf,GAAuB,YAAYzgB,KAAKygB,KACjDe,EAAQ6C,QAAS,GAEZ1H,KAAKuH,MAAK,WACf,MAAMld,EAAO0kB,GAASzJ,oBAAoBtF,KAAM6E,GAChD,GAAsB,iBAAXf,EAAqB,CAC9B,QAA4B,IAAjBzZ,EAAKyZ,GACd,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,IACP,CACF,GACF,EAOFvD,GAAac,GAAGhc,SAAUkpB,GAAwBK,IAAwB,SAAUxP,IAErD,MAAzBA,EAAM7S,OAAOya,SAAmB5H,EAAMW,gBAAmD,MAAjCX,EAAMW,eAAeiH,UAC/E5H,EAAMkD,iBAER,IAAK,MAAM/iB,KAAWqmB,GAAee,gCAAgC3G,MACnE+O,GAASzJ,oBAAoB/lB,EAAS,CACpCmoB,QAAQ,IACPA,QAEP,IAMAvL,GAAmB4S,IAcnB,MAAMqB,GAAS,WAETC,GAAc,eACdC,GAAiB,YAGjBC,GAAiB,UACjBC,GAAmB,YAGnBC,GAAe,OAAOJ,KACtBK,GAAiB,SAASL,KAC1BM,GAAe,OAAON,KACtBO,GAAgB,QAAQP,KACxBQ,GAAyB,QAAQR,KAAcC,KAC/CQ,GAAyB,UAAUT,KAAcC,KACjDS,GAAuB,QAAQV,KAAcC,KAC7CU,GAAoB,OAMpBC,GAAyB,4DACzBC,GAA6B,GAAGD,MAA0BD,KAC1DG,GAAgB,iBAIhBC,GAAgBnV,KAAU,UAAY,YACtCoV,GAAmBpV,KAAU,YAAc,UAC3CqV,GAAmBrV,KAAU,aAAe,eAC5CsV,GAAsBtV,KAAU,eAAiB,aACjDuV,GAAkBvV,KAAU,aAAe,cAC3CwV,GAAiBxV,KAAU,cAAgB,aAG3CyV,GAAY,CAChBC,WAAW,EACX1jB,SAAU,kBACV2jB,QAAS,UACT5pB,OAAQ,CAAC,EAAG,GACZ6pB,aAAc,KACdvzB,UAAW,UAEPwzB,GAAgB,CACpBH,UAAW,mBACX1jB,SAAU,mBACV2jB,QAAS,SACT5pB,OAAQ,0BACR6pB,aAAc,yBACdvzB,UAAW,2BAOb,MAAMyzB,WAAiBrN,GACrB,WAAAP,CAAY5kB,EAASukB,GACnBa,MAAMplB,EAASukB,GACf9D,KAAKgS,QAAU,KACfhS,KAAKiS,QAAUjS,KAAK4E,SAAS7f,WAE7Bib,KAAKkS,MAAQtM,GAAe/gB,KAAKmb,KAAK4E,SAAUuM,IAAe,IAAMvL,GAAeM,KAAKlG,KAAK4E,SAAUuM,IAAe,IAAMvL,GAAeC,QAAQsL,GAAenR,KAAKiS,SACxKjS,KAAKmS,UAAYnS,KAAKoS,eACxB,CAGA,kBAAW1O,GACT,OAAOgO,EACT,CACA,sBAAW/N,GACT,OAAOmO,EACT,CACA,eAAWvV,GACT,OAAO6T,EACT,CAGA,MAAA1I,GACE,OAAO1H,KAAKwP,WAAaxP,KAAKyP,OAASzP,KAAK0P,MAC9C,CACA,IAAAA,GACE,GAAIxU,GAAW8E,KAAK4E,WAAa5E,KAAKwP,WACpC,OAEF,MAAM1P,EAAgB,CACpBA,cAAeE,KAAK4E,UAGtB,IADkBrE,GAAaqB,QAAQ5B,KAAK4E,SAAU+L,GAAc7Q,GACtDkC,iBAAd,CASA,GANAhC,KAAKqS,gBAMD,iBAAkBhtB,SAASC,kBAAoB0a,KAAKiS,QAAQjX,QAzExC,eA0EtB,IAAK,MAAMzb,IAAW,GAAGZ,UAAU0G,SAAS6G,KAAK4Z,UAC/CvF,GAAac,GAAG9hB,EAAS,YAAaqc,IAG1CoE,KAAK4E,SAAS0N,QACdtS,KAAK4E,SAASxjB,aAAa,iBAAiB,GAC5C4e,KAAKkS,MAAM7W,UAAU5E,IAAIua,IACzBhR,KAAK4E,SAASvJ,UAAU5E,IAAIua,IAC5BzQ,GAAaqB,QAAQ5B,KAAK4E,SAAUgM,GAAe9Q,EAhBnD,CAiBF,CACA,IAAA2P,GACE,GAAIvU,GAAW8E,KAAK4E,YAAc5E,KAAKwP,WACrC,OAEF,MAAM1P,EAAgB,CACpBA,cAAeE,KAAK4E,UAEtB5E,KAAKuS,cAAczS,EACrB,CACA,OAAAiF,GACM/E,KAAKgS,SACPhS,KAAKgS,QAAQhZ,UAEf2L,MAAMI,SACR,CACA,MAAAha,GACEiV,KAAKmS,UAAYnS,KAAKoS,gBAClBpS,KAAKgS,SACPhS,KAAKgS,QAAQjnB,QAEjB,CAGA,aAAAwnB,CAAczS,GAEZ,IADkBS,GAAaqB,QAAQ5B,KAAK4E,SAAU6L,GAAc3Q,GACtDkC,iBAAd,CAMA,GAAI,iBAAkB3c,SAASC,gBAC7B,IAAK,MAAM/F,IAAW,GAAGZ,UAAU0G,SAAS6G,KAAK4Z,UAC/CvF,GAAaC,IAAIjhB,EAAS,YAAaqc,IAGvCoE,KAAKgS,SACPhS,KAAKgS,QAAQhZ,UAEfgH,KAAKkS,MAAM7W,UAAU1B,OAAOqX,IAC5BhR,KAAK4E,SAASvJ,UAAU1B,OAAOqX,IAC/BhR,KAAK4E,SAASxjB,aAAa,gBAAiB,SAC5C4hB,GAAYE,oBAAoBlD,KAAKkS,MAAO,UAC5C3R,GAAaqB,QAAQ5B,KAAK4E,SAAU8L,GAAgB5Q,EAhBpD,CAiBF,CACA,UAAA+D,CAAWC,GAET,GAAgC,iBADhCA,EAASa,MAAMd,WAAWC,IACRxlB,YAA2B,GAAUwlB,EAAOxlB,YAAgE,mBAA3CwlB,EAAOxlB,UAAUgF,sBAElG,MAAM,IAAIkhB,UAAU,GAAG4L,GAAO3L,+GAEhC,OAAOX,CACT,CACA,aAAAuO,GACE,QAAsB,IAAX,EACT,MAAM,IAAI7N,UAAU,gEAEtB,IAAIgO,EAAmBxS,KAAK4E,SACG,WAA3B5E,KAAK6E,QAAQvmB,UACfk0B,EAAmBxS,KAAKiS,QACf,GAAUjS,KAAK6E,QAAQvmB,WAChCk0B,EAAmB9X,GAAWsF,KAAK6E,QAAQvmB,WACA,iBAA3B0hB,KAAK6E,QAAQvmB,YAC7Bk0B,EAAmBxS,KAAK6E,QAAQvmB,WAElC,MAAMuzB,EAAe7R,KAAKyS,mBAC1BzS,KAAKgS,QAAU,GAAoBQ,EAAkBxS,KAAKkS,MAAOL,EACnE,CACA,QAAArC,GACE,OAAOxP,KAAKkS,MAAM7W,UAAU7W,SAASwsB,GACvC,CACA,aAAA0B,GACE,MAAMC,EAAiB3S,KAAKiS,QAC5B,GAAIU,EAAetX,UAAU7W,SArKN,WAsKrB,OAAOgtB,GAET,GAAImB,EAAetX,UAAU7W,SAvKJ,aAwKvB,OAAOitB,GAET,GAAIkB,EAAetX,UAAU7W,SAzKA,iBA0K3B,MA5JsB,MA8JxB,GAAImuB,EAAetX,UAAU7W,SA3KE,mBA4K7B,MA9JyB,SAkK3B,MAAMouB,EAAkF,QAA1E3tB,iBAAiB+a,KAAKkS,OAAOpX,iBAAiB,iBAAiB6K,OAC7E,OAAIgN,EAAetX,UAAU7W,SArLP,UAsLbouB,EAAQvB,GAAmBD,GAE7BwB,EAAQrB,GAAsBD,EACvC,CACA,aAAAc,GACE,OAAkD,OAA3CpS,KAAK4E,SAAS5J,QAnLD,UAoLtB,CACA,UAAA6X,GACE,MAAM,OACJ7qB,GACEgY,KAAK6E,QACT,MAAsB,iBAAX7c,EACFA,EAAO9F,MAAM,KAAKY,KAAInF,GAAS4f,OAAO6P,SAASzvB,EAAO,MAEzC,mBAAXqK,EACF8qB,GAAc9qB,EAAO8qB,EAAY9S,KAAK4E,UAExC5c,CACT,CACA,gBAAAyqB,GACE,MAAMM,EAAwB,CAC5Br0B,UAAWshB,KAAK0S,gBAChBtc,UAAW,CAAC,CACV9V,KAAM,kBACNmB,QAAS,CACPwM,SAAU+R,KAAK6E,QAAQ5W,WAExB,CACD3N,KAAM,SACNmB,QAAS,CACPuG,OAAQgY,KAAK6S,iBAanB,OAPI7S,KAAKmS,WAAsC,WAAzBnS,KAAK6E,QAAQ+M,WACjC5O,GAAYC,iBAAiBjD,KAAKkS,MAAO,SAAU,UACnDa,EAAsB3c,UAAY,CAAC,CACjC9V,KAAM,cACNC,SAAS,KAGN,IACFwyB,KACAlW,GAAQmD,KAAK6E,QAAQgN,aAAc,CAACkB,IAE3C,CACA,eAAAC,EAAgB,IACdl2B,EAAG,OACHyP,IAEA,MAAM6f,EAAQxG,GAAezT,KAhOF,8DAgO+B6N,KAAKkS,OAAO/rB,QAAO5G,GAAWob,GAAUpb,KAC7F6sB,EAAM1b,QAMXoN,GAAqBsO,EAAO7f,EAAQzP,IAAQ0zB,IAAmBpE,EAAMhL,SAAS7U,IAAS+lB,OACzF,CAGA,sBAAO7V,CAAgBqH,GACrB,OAAO9D,KAAKuH,MAAK,WACf,MAAMld,EAAO0nB,GAASzM,oBAAoBtF,KAAM8D,GAChD,GAAsB,iBAAXA,EAAX,CAGA,QAA4B,IAAjBzZ,EAAKyZ,GACd,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,IAJL,CAKF,GACF,CACA,iBAAOmP,CAAW7T,GAChB,GA5QuB,IA4QnBA,EAAMuI,QAAgD,UAAfvI,EAAMqB,MA/QnC,QA+QuDrB,EAAMtiB,IACzE,OAEF,MAAMo2B,EAActN,GAAezT,KAAK+e,IACxC,IAAK,MAAMxJ,KAAUwL,EAAa,CAChC,MAAMC,EAAUpB,GAAS1M,YAAYqC,GACrC,IAAKyL,IAAyC,IAA9BA,EAAQtO,QAAQ8M,UAC9B,SAEF,MAAMyB,EAAehU,EAAMgU,eACrBC,EAAeD,EAAahS,SAAS+R,EAAQjB,OACnD,GAAIkB,EAAahS,SAAS+R,EAAQvO,WAA2C,WAA9BuO,EAAQtO,QAAQ8M,YAA2B0B,GAA8C,YAA9BF,EAAQtO,QAAQ8M,WAA2B0B,EACnJ,SAIF,GAAIF,EAAQjB,MAAM1tB,SAAS4a,EAAM7S,UAA2B,UAAf6S,EAAMqB,MA/RvC,QA+R2DrB,EAAMtiB,KAAqB,qCAAqCuG,KAAK+b,EAAM7S,OAAOya,UACvJ,SAEF,MAAMlH,EAAgB,CACpBA,cAAeqT,EAAQvO,UAEN,UAAfxF,EAAMqB,OACRX,EAAciH,WAAa3H,GAE7B+T,EAAQZ,cAAczS,EACxB,CACF,CACA,4BAAOwT,CAAsBlU,GAI3B,MAAMmU,EAAU,kBAAkBlwB,KAAK+b,EAAM7S,OAAOya,SAC9CwM,EAjTW,WAiTKpU,EAAMtiB,IACtB22B,EAAkB,CAAClD,GAAgBC,IAAkBpP,SAAShC,EAAMtiB,KAC1E,IAAK22B,IAAoBD,EACvB,OAEF,GAAID,IAAYC,EACd,OAEFpU,EAAMkD,iBAGN,MAAMoR,EAAkB1T,KAAK+F,QAAQkL,IAA0BjR,KAAO4F,GAAeM,KAAKlG,KAAMiR,IAAwB,IAAMrL,GAAe/gB,KAAKmb,KAAMiR,IAAwB,IAAMrL,GAAeC,QAAQoL,GAAwB7R,EAAMW,eAAehb,YACpPwF,EAAWwnB,GAASzM,oBAAoBoO,GAC9C,GAAID,EAIF,OAHArU,EAAMuU,kBACNppB,EAASmlB,YACTnlB,EAASyoB,gBAAgB5T,GAGvB7U,EAASilB,aAEXpQ,EAAMuU,kBACNppB,EAASklB,OACTiE,EAAgBpB,QAEpB,EAOF/R,GAAac,GAAGhc,SAAUyrB,GAAwBG,GAAwBc,GAASuB,uBACnF/S,GAAac,GAAGhc,SAAUyrB,GAAwBK,GAAeY,GAASuB,uBAC1E/S,GAAac,GAAGhc,SAAUwrB,GAAwBkB,GAASkB,YAC3D1S,GAAac,GAAGhc,SAAU0rB,GAAsBgB,GAASkB,YACzD1S,GAAac,GAAGhc,SAAUwrB,GAAwBI,IAAwB,SAAU7R,GAClFA,EAAMkD,iBACNyP,GAASzM,oBAAoBtF,MAAM0H,QACrC,IAMAvL,GAAmB4V,IAcnB,MAAM6B,GAAS,WAETC,GAAoB,OACpBC,GAAkB,gBAAgBF,KAClCG,GAAY,CAChBC,UAAW,iBACXC,cAAe,KACf7O,YAAY,EACZzK,WAAW,EAEXuZ,YAAa,QAGTC,GAAgB,CACpBH,UAAW,SACXC,cAAe,kBACf7O,WAAY,UACZzK,UAAW,UACXuZ,YAAa,oBAOf,MAAME,WAAiB3Q,GACrB,WAAAU,CAAYL,GACVa,QACA3E,KAAK6E,QAAU7E,KAAK6D,WAAWC,GAC/B9D,KAAKqU,aAAc,EACnBrU,KAAK4E,SAAW,IAClB,CAGA,kBAAWlB,GACT,OAAOqQ,EACT,CACA,sBAAWpQ,GACT,OAAOwQ,EACT,CACA,eAAW5X,GACT,OAAOqX,EACT,CAGA,IAAAlE,CAAKrT,GACH,IAAK2D,KAAK6E,QAAQlK,UAEhB,YADAkC,GAAQR,GAGV2D,KAAKsU,UACL,MAAM/0B,EAAUygB,KAAKuU,cACjBvU,KAAK6E,QAAQO,YACfvJ,GAAOtc,GAETA,EAAQ8b,UAAU5E,IAAIod,IACtB7T,KAAKwU,mBAAkB,KACrB3X,GAAQR,EAAS,GAErB,CACA,IAAAoT,CAAKpT,GACE2D,KAAK6E,QAAQlK,WAIlBqF,KAAKuU,cAAclZ,UAAU1B,OAAOka,IACpC7T,KAAKwU,mBAAkB,KACrBxU,KAAK+E,UACLlI,GAAQR,EAAS,KANjBQ,GAAQR,EAQZ,CACA,OAAA0I,GACO/E,KAAKqU,cAGV9T,GAAaC,IAAIR,KAAK4E,SAAUkP,IAChC9T,KAAK4E,SAASjL,SACdqG,KAAKqU,aAAc,EACrB,CAGA,WAAAE,GACE,IAAKvU,KAAK4E,SAAU,CAClB,MAAM6P,EAAWpvB,SAASqvB,cAAc,OACxCD,EAAST,UAAYhU,KAAK6E,QAAQmP,UAC9BhU,KAAK6E,QAAQO,YACfqP,EAASpZ,UAAU5E,IArFD,QAuFpBuJ,KAAK4E,SAAW6P,CAClB,CACA,OAAOzU,KAAK4E,QACd,CACA,iBAAAZ,CAAkBF,GAGhB,OADAA,EAAOoQ,YAAcxZ,GAAWoJ,EAAOoQ,aAChCpQ,CACT,CACA,OAAAwQ,GACE,GAAItU,KAAKqU,YACP,OAEF,MAAM90B,EAAUygB,KAAKuU,cACrBvU,KAAK6E,QAAQqP,YAAYS,OAAOp1B,GAChCghB,GAAac,GAAG9hB,EAASu0B,IAAiB,KACxCjX,GAAQmD,KAAK6E,QAAQoP,cAAc,IAErCjU,KAAKqU,aAAc,CACrB,CACA,iBAAAG,CAAkBnY,GAChBW,GAAuBX,EAAU2D,KAAKuU,cAAevU,KAAK6E,QAAQO,WACpE,EAeF,MAEMwP,GAAc,gBACdC,GAAkB,UAAUD,KAC5BE,GAAoB,cAAcF,KAGlCG,GAAmB,WACnBC,GAAY,CAChBC,WAAW,EACXC,YAAa,MAGTC,GAAgB,CACpBF,UAAW,UACXC,YAAa,WAOf,MAAME,WAAkB3R,GACtB,WAAAU,CAAYL,GACVa,QACA3E,KAAK6E,QAAU7E,KAAK6D,WAAWC,GAC/B9D,KAAKqV,WAAY,EACjBrV,KAAKsV,qBAAuB,IAC9B,CAGA,kBAAW5R,GACT,OAAOsR,EACT,CACA,sBAAWrR,GACT,OAAOwR,EACT,CACA,eAAW5Y,GACT,MAtCW,WAuCb,CAGA,QAAAgZ,GACMvV,KAAKqV,YAGLrV,KAAK6E,QAAQoQ,WACfjV,KAAK6E,QAAQqQ,YAAY5C,QAE3B/R,GAAaC,IAAInb,SAAUuvB,IAC3BrU,GAAac,GAAGhc,SAAUwvB,IAAiBzV,GAASY,KAAKwV,eAAepW,KACxEmB,GAAac,GAAGhc,SAAUyvB,IAAmB1V,GAASY,KAAKyV,eAAerW,KAC1EY,KAAKqV,WAAY,EACnB,CACA,UAAAK,GACO1V,KAAKqV,YAGVrV,KAAKqV,WAAY,EACjB9U,GAAaC,IAAInb,SAAUuvB,IAC7B,CAGA,cAAAY,CAAepW,GACb,MAAM,YACJ8V,GACElV,KAAK6E,QACT,GAAIzF,EAAM7S,SAAWlH,UAAY+Z,EAAM7S,SAAW2oB,GAAeA,EAAY1wB,SAAS4a,EAAM7S,QAC1F,OAEF,MAAM1L,EAAW+kB,GAAeU,kBAAkB4O,GAC1B,IAApBr0B,EAAS6P,OACXwkB,EAAY5C,QACHtS,KAAKsV,uBAAyBP,GACvCl0B,EAASA,EAAS6P,OAAS,GAAG4hB,QAE9BzxB,EAAS,GAAGyxB,OAEhB,CACA,cAAAmD,CAAerW,GA1ED,QA2ERA,EAAMtiB,MAGVkjB,KAAKsV,qBAAuBlW,EAAMuW,SAAWZ,GA7EzB,UA8EtB,EAeF,MAAMa,GAAyB,oDACzBC,GAA0B,cAC1BC,GAAmB,gBACnBC,GAAkB,eAMxB,MAAMC,GACJ,WAAA7R,GACEnE,KAAK4E,SAAWvf,SAAS6G,IAC3B,CAGA,QAAA+pB,GAEE,MAAMC,EAAgB7wB,SAASC,gBAAgBuC,YAC/C,OAAO1F,KAAKoC,IAAI3E,OAAOu2B,WAAaD,EACtC,CACA,IAAAzG,GACE,MAAM5rB,EAAQmc,KAAKiW,WACnBjW,KAAKoW,mBAELpW,KAAKqW,sBAAsBrW,KAAK4E,SAAUkR,IAAkBQ,GAAmBA,EAAkBzyB,IAEjGmc,KAAKqW,sBAAsBT,GAAwBE,IAAkBQ,GAAmBA,EAAkBzyB,IAC1Gmc,KAAKqW,sBAAsBR,GAAyBE,IAAiBO,GAAmBA,EAAkBzyB,GAC5G,CACA,KAAAwO,GACE2N,KAAKuW,wBAAwBvW,KAAK4E,SAAU,YAC5C5E,KAAKuW,wBAAwBvW,KAAK4E,SAAUkR,IAC5C9V,KAAKuW,wBAAwBX,GAAwBE,IACrD9V,KAAKuW,wBAAwBV,GAAyBE,GACxD,CACA,aAAAS,GACE,OAAOxW,KAAKiW,WAAa,CAC3B,CAGA,gBAAAG,GACEpW,KAAKyW,sBAAsBzW,KAAK4E,SAAU,YAC1C5E,KAAK4E,SAAS7jB,MAAM+K,SAAW,QACjC,CACA,qBAAAuqB,CAAsBtc,EAAU2c,EAAera,GAC7C,MAAMsa,EAAiB3W,KAAKiW,WAS5BjW,KAAK4W,2BAA2B7c,GARHxa,IAC3B,GAAIA,IAAYygB,KAAK4E,UAAYhlB,OAAOu2B,WAAa52B,EAAQsI,YAAc8uB,EACzE,OAEF3W,KAAKyW,sBAAsBl3B,EAASm3B,GACpC,MAAMJ,EAAkB12B,OAAOqF,iBAAiB1F,GAASub,iBAAiB4b,GAC1En3B,EAAQwB,MAAM81B,YAAYH,EAAe,GAAGra,EAASkB,OAAOC,WAAW8Y,QAAsB,GAGjG,CACA,qBAAAG,CAAsBl3B,EAASm3B,GAC7B,MAAMI,EAAcv3B,EAAQwB,MAAM+Z,iBAAiB4b,GAC/CI,GACF9T,GAAYC,iBAAiB1jB,EAASm3B,EAAeI,EAEzD,CACA,uBAAAP,CAAwBxc,EAAU2c,GAWhC1W,KAAK4W,2BAA2B7c,GAVHxa,IAC3B,MAAM5B,EAAQqlB,GAAYQ,iBAAiBjkB,EAASm3B,GAEtC,OAAV/4B,GAIJqlB,GAAYE,oBAAoB3jB,EAASm3B,GACzCn3B,EAAQwB,MAAM81B,YAAYH,EAAe/4B,IAJvC4B,EAAQwB,MAAMg2B,eAAeL,EAIgB,GAGnD,CACA,0BAAAE,CAA2B7c,EAAUid,GACnC,GAAI,GAAUjd,GACZid,EAASjd,QAGX,IAAK,MAAMkd,KAAOrR,GAAezT,KAAK4H,EAAUiG,KAAK4E,UACnDoS,EAASC,EAEb,EAeF,MAEMC,GAAc,YAGdC,GAAe,OAAOD,KACtBE,GAAyB,gBAAgBF,KACzCG,GAAiB,SAASH,KAC1BI,GAAe,OAAOJ,KACtBK,GAAgB,QAAQL,KACxBM,GAAiB,SAASN,KAC1BO,GAAsB,gBAAgBP,KACtCQ,GAA0B,oBAAoBR,KAC9CS,GAA0B,kBAAkBT,KAC5CU,GAAyB,QAAQV,cACjCW,GAAkB,aAElBC,GAAoB,OACpBC,GAAoB,eAKpBC,GAAY,CAChBvD,UAAU,EACVnC,OAAO,EACPzH,UAAU,GAENoN,GAAgB,CACpBxD,SAAU,mBACVnC,MAAO,UACPzH,SAAU,WAOZ,MAAMqN,WAAcxT,GAClB,WAAAP,CAAY5kB,EAASukB,GACnBa,MAAMplB,EAASukB,GACf9D,KAAKmY,QAAUvS,GAAeC,QArBV,gBAqBmC7F,KAAK4E,UAC5D5E,KAAKoY,UAAYpY,KAAKqY,sBACtBrY,KAAKsY,WAAatY,KAAKuY,uBACvBvY,KAAKwP,UAAW,EAChBxP,KAAKgP,kBAAmB,EACxBhP,KAAKwY,WAAa,IAAIxC,GACtBhW,KAAK0L,oBACP,CAGA,kBAAWhI,GACT,OAAOsU,EACT,CACA,sBAAWrU,GACT,OAAOsU,EACT,CACA,eAAW1b,GACT,MA1DW,OA2Db,CAGA,MAAAmL,CAAO5H,GACL,OAAOE,KAAKwP,SAAWxP,KAAKyP,OAASzP,KAAK0P,KAAK5P,EACjD,CACA,IAAA4P,CAAK5P,GACCE,KAAKwP,UAAYxP,KAAKgP,kBAGRzO,GAAaqB,QAAQ5B,KAAK4E,SAAU0S,GAAc,CAClExX,kBAEYkC,mBAGdhC,KAAKwP,UAAW,EAChBxP,KAAKgP,kBAAmB,EACxBhP,KAAKwY,WAAW/I,OAChBpqB,SAAS6G,KAAKmP,UAAU5E,IAAIohB,IAC5B7X,KAAKyY,gBACLzY,KAAKoY,UAAU1I,MAAK,IAAM1P,KAAK0Y,aAAa5Y,KAC9C,CACA,IAAA2P,GACOzP,KAAKwP,WAAYxP,KAAKgP,mBAGTzO,GAAaqB,QAAQ5B,KAAK4E,SAAUuS,IACxCnV,mBAGdhC,KAAKwP,UAAW,EAChBxP,KAAKgP,kBAAmB,EACxBhP,KAAKsY,WAAW5C,aAChB1V,KAAK4E,SAASvJ,UAAU1B,OAAOme,IAC/B9X,KAAKmF,gBAAe,IAAMnF,KAAK2Y,cAAc3Y,KAAK4E,SAAU5E,KAAK6N,gBACnE,CACA,OAAA9I,GACExE,GAAaC,IAAI5gB,OAAQs3B,IACzB3W,GAAaC,IAAIR,KAAKmY,QAASjB,IAC/BlX,KAAKoY,UAAUrT,UACf/E,KAAKsY,WAAW5C,aAChB/Q,MAAMI,SACR,CACA,YAAA6T,GACE5Y,KAAKyY,eACP,CAGA,mBAAAJ,GACE,OAAO,IAAIjE,GAAS,CAClBzZ,UAAWmG,QAAQd,KAAK6E,QAAQ4P,UAEhCrP,WAAYpF,KAAK6N,eAErB,CACA,oBAAA0K,GACE,OAAO,IAAInD,GAAU,CACnBF,YAAalV,KAAK4E,UAEtB,CACA,YAAA8T,CAAa5Y,GAENza,SAAS6G,KAAK1H,SAASwb,KAAK4E,WAC/Bvf,SAAS6G,KAAKyoB,OAAO3U,KAAK4E,UAE5B5E,KAAK4E,SAAS7jB,MAAM6wB,QAAU,QAC9B5R,KAAK4E,SAASzjB,gBAAgB,eAC9B6e,KAAK4E,SAASxjB,aAAa,cAAc,GACzC4e,KAAK4E,SAASxjB,aAAa,OAAQ,UACnC4e,KAAK4E,SAASnZ,UAAY,EAC1B,MAAMotB,EAAYjT,GAAeC,QA7GT,cA6GsC7F,KAAKmY,SAC/DU,IACFA,EAAUptB,UAAY,GAExBoQ,GAAOmE,KAAK4E,UACZ5E,KAAK4E,SAASvJ,UAAU5E,IAAIqhB,IAU5B9X,KAAKmF,gBATsB,KACrBnF,KAAK6E,QAAQyN,OACftS,KAAKsY,WAAW/C,WAElBvV,KAAKgP,kBAAmB,EACxBzO,GAAaqB,QAAQ5B,KAAK4E,SAAU2S,GAAe,CACjDzX,iBACA,GAEoCE,KAAKmY,QAASnY,KAAK6N,cAC7D,CACA,kBAAAnC,GACEnL,GAAac,GAAGrB,KAAK4E,SAAU+S,IAAyBvY,IAhJvC,WAiJXA,EAAMtiB,MAGNkjB,KAAK6E,QAAQgG,SACf7K,KAAKyP,OAGPzP,KAAK8Y,6BAA4B,IAEnCvY,GAAac,GAAGzhB,OAAQ43B,IAAgB,KAClCxX,KAAKwP,WAAaxP,KAAKgP,kBACzBhP,KAAKyY,eACP,IAEFlY,GAAac,GAAGrB,KAAK4E,SAAU8S,IAAyBtY,IAEtDmB,GAAae,IAAItB,KAAK4E,SAAU6S,IAAqBsB,IAC/C/Y,KAAK4E,WAAaxF,EAAM7S,QAAUyT,KAAK4E,WAAamU,EAAOxsB,SAGjC,WAA1ByT,KAAK6E,QAAQ4P,SAIbzU,KAAK6E,QAAQ4P,UACfzU,KAAKyP,OAJLzP,KAAK8Y,6BAKP,GACA,GAEN,CACA,UAAAH,GACE3Y,KAAK4E,SAAS7jB,MAAM6wB,QAAU,OAC9B5R,KAAK4E,SAASxjB,aAAa,eAAe,GAC1C4e,KAAK4E,SAASzjB,gBAAgB,cAC9B6e,KAAK4E,SAASzjB,gBAAgB,QAC9B6e,KAAKgP,kBAAmB,EACxBhP,KAAKoY,UAAU3I,MAAK,KAClBpqB,SAAS6G,KAAKmP,UAAU1B,OAAOke,IAC/B7X,KAAKgZ,oBACLhZ,KAAKwY,WAAWnmB,QAChBkO,GAAaqB,QAAQ5B,KAAK4E,SAAUyS,GAAe,GAEvD,CACA,WAAAxJ,GACE,OAAO7N,KAAK4E,SAASvJ,UAAU7W,SAjLT,OAkLxB,CACA,0BAAAs0B,GAEE,GADkBvY,GAAaqB,QAAQ5B,KAAK4E,SAAUwS,IACxCpV,iBACZ,OAEF,MAAMiX,EAAqBjZ,KAAK4E,SAASvX,aAAehI,SAASC,gBAAgBsC,aAC3EsxB,EAAmBlZ,KAAK4E,SAAS7jB,MAAMiL,UAEpB,WAArBktB,GAAiClZ,KAAK4E,SAASvJ,UAAU7W,SAASuzB,MAGjEkB,IACHjZ,KAAK4E,SAAS7jB,MAAMiL,UAAY,UAElCgU,KAAK4E,SAASvJ,UAAU5E,IAAIshB,IAC5B/X,KAAKmF,gBAAe,KAClBnF,KAAK4E,SAASvJ,UAAU1B,OAAOoe,IAC/B/X,KAAKmF,gBAAe,KAClBnF,KAAK4E,SAAS7jB,MAAMiL,UAAYktB,CAAgB,GAC/ClZ,KAAKmY,QAAQ,GACfnY,KAAKmY,SACRnY,KAAK4E,SAAS0N,QAChB,CAMA,aAAAmG,GACE,MAAMQ,EAAqBjZ,KAAK4E,SAASvX,aAAehI,SAASC,gBAAgBsC,aAC3E+uB,EAAiB3W,KAAKwY,WAAWvC,WACjCkD,EAAoBxC,EAAiB,EAC3C,GAAIwC,IAAsBF,EAAoB,CAC5C,MAAMn3B,EAAWma,KAAU,cAAgB,eAC3C+D,KAAK4E,SAAS7jB,MAAMe,GAAY,GAAG60B,KACrC,CACA,IAAKwC,GAAqBF,EAAoB,CAC5C,MAAMn3B,EAAWma,KAAU,eAAiB,cAC5C+D,KAAK4E,SAAS7jB,MAAMe,GAAY,GAAG60B,KACrC,CACF,CACA,iBAAAqC,GACEhZ,KAAK4E,SAAS7jB,MAAMq4B,YAAc,GAClCpZ,KAAK4E,SAAS7jB,MAAMs4B,aAAe,EACrC,CAGA,sBAAO5c,CAAgBqH,EAAQhE,GAC7B,OAAOE,KAAKuH,MAAK,WACf,MAAMld,EAAO6tB,GAAM5S,oBAAoBtF,KAAM8D,GAC7C,GAAsB,iBAAXA,EAAX,CAGA,QAA4B,IAAjBzZ,EAAKyZ,GACd,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,GAAQhE,EAJb,CAKF,GACF,EAOFS,GAAac,GAAGhc,SAAUuyB,GA9OK,4BA8O2C,SAAUxY,GAClF,MAAM7S,EAASqZ,GAAec,uBAAuB1G,MACjD,CAAC,IAAK,QAAQoB,SAASpB,KAAKgH,UAC9B5H,EAAMkD,iBAER/B,GAAae,IAAI/U,EAAQ+qB,IAAcgC,IACjCA,EAAUtX,kBAIdzB,GAAae,IAAI/U,EAAQ8qB,IAAgB,KACnC1c,GAAUqF,OACZA,KAAKsS,OACP,GACA,IAIJ,MAAMiH,EAAc3T,GAAeC,QAnQb,eAoQlB0T,GACFrB,GAAM7S,YAAYkU,GAAa9J,OAEpByI,GAAM5S,oBAAoB/Y,GAClCmb,OAAO1H,KACd,IACA4G,GAAqBsR,IAMrB/b,GAAmB+b,IAcnB,MAEMsB,GAAc,gBACdC,GAAiB,YACjBC,GAAwB,OAAOF,KAAcC,KAE7CE,GAAoB,OACpBC,GAAuB,UACvBC,GAAoB,SAEpBC,GAAgB,kBAChBC,GAAe,OAAOP,KACtBQ,GAAgB,QAAQR,KACxBS,GAAe,OAAOT,KACtBU,GAAuB,gBAAgBV,KACvCW,GAAiB,SAASX,KAC1BY,GAAe,SAASZ,KACxBa,GAAyB,QAAQb,KAAcC,KAC/Ca,GAAwB,kBAAkBd,KAE1Ce,GAAY,CAChB9F,UAAU,EACV5J,UAAU,EACVpgB,QAAQ,GAEJ+vB,GAAgB,CACpB/F,SAAU,mBACV5J,SAAU,UACVpgB,OAAQ,WAOV,MAAMgwB,WAAkB/V,GACtB,WAAAP,CAAY5kB,EAASukB,GACnBa,MAAMplB,EAASukB,GACf9D,KAAKwP,UAAW,EAChBxP,KAAKoY,UAAYpY,KAAKqY,sBACtBrY,KAAKsY,WAAatY,KAAKuY,uBACvBvY,KAAK0L,oBACP,CAGA,kBAAWhI,GACT,OAAO6W,EACT,CACA,sBAAW5W,GACT,OAAO6W,EACT,CACA,eAAWje,GACT,MApDW,WAqDb,CAGA,MAAAmL,CAAO5H,GACL,OAAOE,KAAKwP,SAAWxP,KAAKyP,OAASzP,KAAK0P,KAAK5P,EACjD,CACA,IAAA4P,CAAK5P,GACCE,KAAKwP,UAGSjP,GAAaqB,QAAQ5B,KAAK4E,SAAUmV,GAAc,CAClEja,kBAEYkC,mBAGdhC,KAAKwP,UAAW,EAChBxP,KAAKoY,UAAU1I,OACV1P,KAAK6E,QAAQpa,SAChB,IAAIurB,IAAkBvG,OAExBzP,KAAK4E,SAASxjB,aAAa,cAAc,GACzC4e,KAAK4E,SAASxjB,aAAa,OAAQ,UACnC4e,KAAK4E,SAASvJ,UAAU5E,IAAImjB,IAW5B5Z,KAAKmF,gBAVoB,KAClBnF,KAAK6E,QAAQpa,SAAUuV,KAAK6E,QAAQ4P,UACvCzU,KAAKsY,WAAW/C,WAElBvV,KAAK4E,SAASvJ,UAAU5E,IAAIkjB,IAC5B3Z,KAAK4E,SAASvJ,UAAU1B,OAAOigB,IAC/BrZ,GAAaqB,QAAQ5B,KAAK4E,SAAUoV,GAAe,CACjDla,iBACA,GAEkCE,KAAK4E,UAAU,GACvD,CACA,IAAA6K,GACOzP,KAAKwP,WAGQjP,GAAaqB,QAAQ5B,KAAK4E,SAAUqV,IACxCjY,mBAGdhC,KAAKsY,WAAW5C,aAChB1V,KAAK4E,SAAS8V,OACd1a,KAAKwP,UAAW,EAChBxP,KAAK4E,SAASvJ,UAAU5E,IAAIojB,IAC5B7Z,KAAKoY,UAAU3I,OAUfzP,KAAKmF,gBAToB,KACvBnF,KAAK4E,SAASvJ,UAAU1B,OAAOggB,GAAmBE,IAClD7Z,KAAK4E,SAASzjB,gBAAgB,cAC9B6e,KAAK4E,SAASzjB,gBAAgB,QACzB6e,KAAK6E,QAAQpa,SAChB,IAAIurB,IAAkB3jB,QAExBkO,GAAaqB,QAAQ5B,KAAK4E,SAAUuV,GAAe,GAEfna,KAAK4E,UAAU,IACvD,CACA,OAAAG,GACE/E,KAAKoY,UAAUrT,UACf/E,KAAKsY,WAAW5C,aAChB/Q,MAAMI,SACR,CAGA,mBAAAsT,GACE,MASM1d,EAAYmG,QAAQd,KAAK6E,QAAQ4P,UACvC,OAAO,IAAIL,GAAS,CAClBJ,UA3HsB,qBA4HtBrZ,YACAyK,YAAY,EACZ8O,YAAalU,KAAK4E,SAAS7f,WAC3BkvB,cAAetZ,EAfK,KACU,WAA1BqF,KAAK6E,QAAQ4P,SAIjBzU,KAAKyP,OAHHlP,GAAaqB,QAAQ5B,KAAK4E,SAAUsV,GAG3B,EAUgC,MAE/C,CACA,oBAAA3B,GACE,OAAO,IAAInD,GAAU,CACnBF,YAAalV,KAAK4E,UAEtB,CACA,kBAAA8G,GACEnL,GAAac,GAAGrB,KAAK4E,SAAU0V,IAAuBlb,IA5IvC,WA6ITA,EAAMtiB,MAGNkjB,KAAK6E,QAAQgG,SACf7K,KAAKyP,OAGPlP,GAAaqB,QAAQ5B,KAAK4E,SAAUsV,IAAqB,GAE7D,CAGA,sBAAOzd,CAAgBqH,GACrB,OAAO9D,KAAKuH,MAAK,WACf,MAAMld,EAAOowB,GAAUnV,oBAAoBtF,KAAM8D,GACjD,GAAsB,iBAAXA,EAAX,CAGA,QAAqB/K,IAAjB1O,EAAKyZ,IAAyBA,EAAOrC,WAAW,MAAmB,gBAAXqC,EAC1D,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,GAAQ9D,KAJb,CAKF,GACF,EAOFO,GAAac,GAAGhc,SAAUg1B,GA7JK,gCA6J2C,SAAUjb,GAClF,MAAM7S,EAASqZ,GAAec,uBAAuB1G,MAIrD,GAHI,CAAC,IAAK,QAAQoB,SAASpB,KAAKgH,UAC9B5H,EAAMkD,iBAEJpH,GAAW8E,MACb,OAEFO,GAAae,IAAI/U,EAAQ4tB,IAAgB,KAEnCxf,GAAUqF,OACZA,KAAKsS,OACP,IAIF,MAAMiH,EAAc3T,GAAeC,QAAQiU,IACvCP,GAAeA,IAAgBhtB,GACjCkuB,GAAUpV,YAAYkU,GAAa9J,OAExBgL,GAAUnV,oBAAoB/Y,GACtCmb,OAAO1H,KACd,IACAO,GAAac,GAAGzhB,OAAQ85B,IAAuB,KAC7C,IAAK,MAAM3f,KAAY6L,GAAezT,KAAK2nB,IACzCW,GAAUnV,oBAAoBvL,GAAU2V,MAC1C,IAEFnP,GAAac,GAAGzhB,OAAQw6B,IAAc,KACpC,IAAK,MAAM76B,KAAWqmB,GAAezT,KAAK,gDACG,UAAvClN,iBAAiB1F,GAASiC,UAC5Bi5B,GAAUnV,oBAAoB/lB,GAASkwB,MAE3C,IAEF7I,GAAqB6T,IAMrBte,GAAmBse,IAUnB,MACME,GAAmB,CAEvB,IAAK,CAAC,QAAS,MAAO,KAAM,OAAQ,OAHP,kBAI7B9pB,EAAG,CAAC,SAAU,OAAQ,QAAS,OAC/B+pB,KAAM,GACN9pB,EAAG,GACH+pB,GAAI,GACJC,IAAK,GACLC,KAAM,GACNC,IAAK,GACLC,GAAI,GACJC,GAAI,GACJC,GAAI,GACJC,GAAI,GACJC,GAAI,GACJC,GAAI,GACJC,GAAI,GACJC,GAAI,GACJnqB,EAAG,GACHub,IAAK,CAAC,MAAO,SAAU,MAAO,QAAS,QAAS,UAChD6O,GAAI,GACJC,GAAI,GACJC,EAAG,GACHC,IAAK,GACLC,EAAG,GACHC,MAAO,GACPC,KAAM,GACNC,IAAK,GACLC,IAAK,GACLC,OAAQ,GACRC,EAAG,GACHC,GAAI,IAIAC,GAAgB,IAAI/lB,IAAI,CAAC,aAAc,OAAQ,OAAQ,WAAY,WAAY,SAAU,MAAO,eAShGgmB,GAAmB,0DACnBC,GAAmB,CAACx6B,EAAWy6B,KACnC,MAAMC,EAAgB16B,EAAUvC,SAASC,cACzC,OAAI+8B,EAAqBpb,SAASqb,IAC5BJ,GAAc1lB,IAAI8lB,IACb3b,QAAQwb,GAAiBj5B,KAAKtB,EAAU26B,YAM5CF,EAAqBr2B,QAAOw2B,GAAkBA,aAA0BpY,SAAQ9R,MAAKmqB,GAASA,EAAMv5B,KAAKo5B,IAAe,EA0C3HI,GAAY,CAChBC,UAAWnC,GACXoC,QAAS,CAAC,EAEVC,WAAY,GACZnwB,MAAM,EACNowB,UAAU,EACVC,WAAY,KACZC,SAAU,eAENC,GAAgB,CACpBN,UAAW,SACXC,QAAS,SACTC,WAAY,oBACZnwB,KAAM,UACNowB,SAAU,UACVC,WAAY,kBACZC,SAAU,UAENE,GAAqB,CACzBC,MAAO,iCACPvjB,SAAU,oBAOZ,MAAMwjB,WAAwB9Z,GAC5B,WAAAU,CAAYL,GACVa,QACA3E,KAAK6E,QAAU7E,KAAK6D,WAAWC,EACjC,CAGA,kBAAWJ,GACT,OAAOmZ,EACT,CACA,sBAAWlZ,GACT,OAAOyZ,EACT,CACA,eAAW7gB,GACT,MA3CW,iBA4Cb,CAGA,UAAAihB,GACE,OAAOxgC,OAAOmiB,OAAOa,KAAK6E,QAAQkY,SAASj6B,KAAIghB,GAAU9D,KAAKyd,yBAAyB3Z,KAAS3d,OAAO2a,QACzG,CACA,UAAA4c,GACE,OAAO1d,KAAKwd,aAAa9sB,OAAS,CACpC,CACA,aAAAitB,CAAcZ,GAMZ,OALA/c,KAAK4d,cAAcb,GACnB/c,KAAK6E,QAAQkY,QAAU,IAClB/c,KAAK6E,QAAQkY,WACbA,GAEE/c,IACT,CACA,MAAA6d,GACE,MAAMC,EAAkBz4B,SAASqvB,cAAc,OAC/CoJ,EAAgBC,UAAY/d,KAAKge,eAAehe,KAAK6E,QAAQsY,UAC7D,IAAK,MAAOpjB,EAAUkkB,KAASjhC,OAAOmkB,QAAQnB,KAAK6E,QAAQkY,SACzD/c,KAAKke,YAAYJ,EAAiBG,EAAMlkB,GAE1C,MAAMojB,EAAWW,EAAgBhY,SAAS,GACpCkX,EAAahd,KAAKyd,yBAAyBzd,KAAK6E,QAAQmY,YAI9D,OAHIA,GACFG,EAAS9hB,UAAU5E,OAAOumB,EAAW96B,MAAM,MAEtCi7B,CACT,CAGA,gBAAAlZ,CAAiBH,GACfa,MAAMV,iBAAiBH,GACvB9D,KAAK4d,cAAc9Z,EAAOiZ,QAC5B,CACA,aAAAa,CAAcO,GACZ,IAAK,MAAOpkB,EAAUgjB,KAAY//B,OAAOmkB,QAAQgd,GAC/CxZ,MAAMV,iBAAiB,CACrBlK,WACAujB,MAAOP,GACNM,GAEP,CACA,WAAAa,CAAYf,EAAUJ,EAAShjB,GAC7B,MAAMqkB,EAAkBxY,GAAeC,QAAQ9L,EAAUojB,GACpDiB,KAGLrB,EAAU/c,KAAKyd,yBAAyBV,IAKpC,GAAUA,GACZ/c,KAAKqe,sBAAsB3jB,GAAWqiB,GAAUqB,GAG9Cpe,KAAK6E,QAAQhY,KACfuxB,EAAgBL,UAAY/d,KAAKge,eAAejB,GAGlDqB,EAAgBE,YAAcvB,EAX5BqB,EAAgBzkB,SAYpB,CACA,cAAAqkB,CAAeG,GACb,OAAOne,KAAK6E,QAAQoY,SApJxB,SAAsBsB,EAAYzB,EAAW0B,GAC3C,IAAKD,EAAW7tB,OACd,OAAO6tB,EAET,GAAIC,GAAgD,mBAArBA,EAC7B,OAAOA,EAAiBD,GAE1B,MACME,GADY,IAAI7+B,OAAO8+B,WACKC,gBAAgBJ,EAAY,aACxD19B,EAAW,GAAGlC,UAAU8/B,EAAgBvyB,KAAKkU,iBAAiB,MACpE,IAAK,MAAM7gB,KAAWsB,EAAU,CAC9B,MAAM+9B,EAAcr/B,EAAQC,SAASC,cACrC,IAAKzC,OAAO4D,KAAKk8B,GAAW1b,SAASwd,GAAc,CACjDr/B,EAAQoa,SACR,QACF,CACA,MAAMklB,EAAgB,GAAGlgC,UAAUY,EAAQ0B,YACrC69B,EAAoB,GAAGngC,OAAOm+B,EAAU,MAAQ,GAAIA,EAAU8B,IAAgB,IACpF,IAAK,MAAM78B,KAAa88B,EACjBtC,GAAiBx6B,EAAW+8B,IAC/Bv/B,EAAQ4B,gBAAgBY,EAAUvC,SAGxC,CACA,OAAOi/B,EAAgBvyB,KAAK6xB,SAC9B,CA2HmCgB,CAAaZ,EAAKne,KAAK6E,QAAQiY,UAAW9c,KAAK6E,QAAQqY,YAAciB,CACtG,CACA,wBAAAV,CAAyBU,GACvB,OAAOthB,GAAQshB,EAAK,CAACne,MACvB,CACA,qBAAAqe,CAAsB9+B,EAAS6+B,GAC7B,GAAIpe,KAAK6E,QAAQhY,KAGf,OAFAuxB,EAAgBL,UAAY,QAC5BK,EAAgBzJ,OAAOp1B,GAGzB6+B,EAAgBE,YAAc/+B,EAAQ++B,WACxC,EAeF,MACMU,GAAwB,IAAI1oB,IAAI,CAAC,WAAY,YAAa,eAC1D2oB,GAAoB,OAEpBC,GAAoB,OAEpBC,GAAiB,SACjBC,GAAmB,gBACnBC,GAAgB,QAChBC,GAAgB,QAahBC,GAAgB,CACpBC,KAAM,OACNC,IAAK,MACLC,MAAOzjB,KAAU,OAAS,QAC1B0jB,OAAQ,SACRC,KAAM3jB,KAAU,QAAU,QAEtB4jB,GAAY,CAChB/C,UAAWnC,GACXmF,WAAW,EACX7xB,SAAU,kBACV8xB,WAAW,EACXC,YAAa,GACbC,MAAO,EACPjwB,mBAAoB,CAAC,MAAO,QAAS,SAAU,QAC/CnD,MAAM,EACN7E,OAAQ,CAAC,EAAG,GACZtJ,UAAW,MACXmzB,aAAc,KACdoL,UAAU,EACVC,WAAY,KACZnjB,UAAU,EACVojB,SAAU,+GACV+C,MAAO,GACPte,QAAS,eAELue,GAAgB,CACpBrD,UAAW,SACXgD,UAAW,UACX7xB,SAAU,mBACV8xB,UAAW,2BACXC,YAAa,oBACbC,MAAO,kBACPjwB,mBAAoB,QACpBnD,KAAM,UACN7E,OAAQ,0BACRtJ,UAAW,oBACXmzB,aAAc,yBACdoL,SAAU,UACVC,WAAY,kBACZnjB,SAAU,mBACVojB,SAAU,SACV+C,MAAO,4BACPte,QAAS,UAOX,MAAMwe,WAAgB1b,GACpB,WAAAP,CAAY5kB,EAASukB,GACnB,QAAsB,IAAX,EACT,MAAM,IAAIU,UAAU,+DAEtBG,MAAMplB,EAASukB,GAGf9D,KAAKqgB,YAAa,EAClBrgB,KAAKsgB,SAAW,EAChBtgB,KAAKugB,WAAa,KAClBvgB,KAAKwgB,eAAiB,CAAC,EACvBxgB,KAAKgS,QAAU,KACfhS,KAAKygB,iBAAmB,KACxBzgB,KAAK0gB,YAAc,KAGnB1gB,KAAK2gB,IAAM,KACX3gB,KAAK4gB,gBACA5gB,KAAK6E,QAAQ9K,UAChBiG,KAAK6gB,WAET,CAGA,kBAAWnd,GACT,OAAOmc,EACT,CACA,sBAAWlc,GACT,OAAOwc,EACT,CACA,eAAW5jB,GACT,MAxGW,SAyGb,CAGA,MAAAukB,GACE9gB,KAAKqgB,YAAa,CACpB,CACA,OAAAU,GACE/gB,KAAKqgB,YAAa,CACpB,CACA,aAAAW,GACEhhB,KAAKqgB,YAAcrgB,KAAKqgB,UAC1B,CACA,MAAA3Y,GACO1H,KAAKqgB,aAGVrgB,KAAKwgB,eAAeS,OAASjhB,KAAKwgB,eAAeS,MAC7CjhB,KAAKwP,WACPxP,KAAKkhB,SAGPlhB,KAAKmhB,SACP,CACA,OAAApc,GACEgI,aAAa/M,KAAKsgB,UAClB/f,GAAaC,IAAIR,KAAK4E,SAAS5J,QAAQmkB,IAAiBC,GAAkBpf,KAAKohB,mBAC3EphB,KAAK4E,SAASpJ,aAAa,2BAC7BwE,KAAK4E,SAASxjB,aAAa,QAAS4e,KAAK4E,SAASpJ,aAAa,2BAEjEwE,KAAKqhB,iBACL1c,MAAMI,SACR,CACA,IAAA2K,GACE,GAAoC,SAAhC1P,KAAK4E,SAAS7jB,MAAM6wB,QACtB,MAAM,IAAIhO,MAAM,uCAElB,IAAM5D,KAAKshB,mBAAoBthB,KAAKqgB,WAClC,OAEF,MAAM/G,EAAY/Y,GAAaqB,QAAQ5B,KAAK4E,SAAU5E,KAAKmE,YAAYqB,UAlItD,SAoIX+b,GADa9lB,GAAeuE,KAAK4E,WACL5E,KAAK4E,SAAS9kB,cAAcwF,iBAAiBd,SAASwb,KAAK4E,UAC7F,GAAI0U,EAAUtX,mBAAqBuf,EACjC,OAIFvhB,KAAKqhB,iBACL,MAAMV,EAAM3gB,KAAKwhB,iBACjBxhB,KAAK4E,SAASxjB,aAAa,mBAAoBu/B,EAAInlB,aAAa,OAChE,MAAM,UACJukB,GACE/f,KAAK6E,QAYT,GAXK7E,KAAK4E,SAAS9kB,cAAcwF,gBAAgBd,SAASwb,KAAK2gB,OAC7DZ,EAAUpL,OAAOgM,GACjBpgB,GAAaqB,QAAQ5B,KAAK4E,SAAU5E,KAAKmE,YAAYqB,UAhJpC,cAkJnBxF,KAAKgS,QAAUhS,KAAKqS,cAAcsO,GAClCA,EAAItlB,UAAU5E,IAAIyoB,IAMd,iBAAkB75B,SAASC,gBAC7B,IAAK,MAAM/F,IAAW,GAAGZ,UAAU0G,SAAS6G,KAAK4Z,UAC/CvF,GAAac,GAAG9hB,EAAS,YAAaqc,IAU1CoE,KAAKmF,gBAPY,KACf5E,GAAaqB,QAAQ5B,KAAK4E,SAAU5E,KAAKmE,YAAYqB,UAhKrC,WAiKQ,IAApBxF,KAAKugB,YACPvgB,KAAKkhB,SAEPlhB,KAAKugB,YAAa,CAAK,GAEKvgB,KAAK2gB,IAAK3gB,KAAK6N,cAC/C,CACA,IAAA4B,GACE,GAAKzP,KAAKwP,aAGQjP,GAAaqB,QAAQ5B,KAAK4E,SAAU5E,KAAKmE,YAAYqB,UA/KtD,SAgLHxD,iBAAd,CAQA,GALYhC,KAAKwhB,iBACbnmB,UAAU1B,OAAOulB,IAIjB,iBAAkB75B,SAASC,gBAC7B,IAAK,MAAM/F,IAAW,GAAGZ,UAAU0G,SAAS6G,KAAK4Z,UAC/CvF,GAAaC,IAAIjhB,EAAS,YAAaqc,IAG3CoE,KAAKwgB,eAA4B,OAAI,EACrCxgB,KAAKwgB,eAAelB,KAAiB,EACrCtf,KAAKwgB,eAAenB,KAAiB,EACrCrf,KAAKugB,WAAa,KAYlBvgB,KAAKmF,gBAVY,KACXnF,KAAKyhB,yBAGJzhB,KAAKugB,YACRvgB,KAAKqhB,iBAEPrhB,KAAK4E,SAASzjB,gBAAgB,oBAC9Bof,GAAaqB,QAAQ5B,KAAK4E,SAAU5E,KAAKmE,YAAYqB,UAzMpC,WAyM8D,GAEnDxF,KAAK2gB,IAAK3gB,KAAK6N,cA1B7C,CA2BF,CACA,MAAA9iB,GACMiV,KAAKgS,SACPhS,KAAKgS,QAAQjnB,QAEjB,CAGA,cAAAu2B,GACE,OAAOxgB,QAAQd,KAAK0hB,YACtB,CACA,cAAAF,GAIE,OAHKxhB,KAAK2gB,MACR3gB,KAAK2gB,IAAM3gB,KAAK2hB,kBAAkB3hB,KAAK0gB,aAAe1gB,KAAK4hB,2BAEtD5hB,KAAK2gB,GACd,CACA,iBAAAgB,CAAkB5E,GAChB,MAAM4D,EAAM3gB,KAAK6hB,oBAAoB9E,GAASc,SAG9C,IAAK8C,EACH,OAAO,KAETA,EAAItlB,UAAU1B,OAAOslB,GAAmBC,IAExCyB,EAAItlB,UAAU5E,IAAI,MAAMuJ,KAAKmE,YAAY5H,aACzC,MAAMulB,EAvuGKC,KACb,GACEA,GAAU5/B,KAAK6/B,MA/BH,IA+BS7/B,KAAK8/B,gBACnB58B,SAAS68B,eAAeH,IACjC,OAAOA,CAAM,EAmuGGI,CAAOniB,KAAKmE,YAAY5H,MAAM1c,WAK5C,OAJA8gC,EAAIv/B,aAAa,KAAM0gC,GACnB9hB,KAAK6N,eACP8S,EAAItlB,UAAU5E,IAAIwoB,IAEb0B,CACT,CACA,UAAAyB,CAAWrF,GACT/c,KAAK0gB,YAAc3D,EACf/c,KAAKwP,aACPxP,KAAKqhB,iBACLrhB,KAAK0P,OAET,CACA,mBAAAmS,CAAoB9E,GAYlB,OAXI/c,KAAKygB,iBACPzgB,KAAKygB,iBAAiB9C,cAAcZ,GAEpC/c,KAAKygB,iBAAmB,IAAIlD,GAAgB,IACvCvd,KAAK6E,QAGRkY,UACAC,WAAYhd,KAAKyd,yBAAyBzd,KAAK6E,QAAQmb,eAGpDhgB,KAAKygB,gBACd,CACA,sBAAAmB,GACE,MAAO,CACL,iBAA0B5hB,KAAK0hB,YAEnC,CACA,SAAAA,GACE,OAAO1hB,KAAKyd,yBAAyBzd,KAAK6E,QAAQqb,QAAUlgB,KAAK4E,SAASpJ,aAAa,yBACzF,CAGA,4BAAA6mB,CAA6BjjB,GAC3B,OAAOY,KAAKmE,YAAYmB,oBAAoBlG,EAAMW,eAAgBC,KAAKsiB,qBACzE,CACA,WAAAzU,GACE,OAAO7N,KAAK6E,QAAQib,WAAa9f,KAAK2gB,KAAO3gB,KAAK2gB,IAAItlB,UAAU7W,SAASy6B,GAC3E,CACA,QAAAzP,GACE,OAAOxP,KAAK2gB,KAAO3gB,KAAK2gB,IAAItlB,UAAU7W,SAAS06B,GACjD,CACA,aAAA7M,CAAcsO,GACZ,MAAMjiC,EAAYme,GAAQmD,KAAK6E,QAAQnmB,UAAW,CAACshB,KAAM2gB,EAAK3gB,KAAK4E,WAC7D2d,EAAahD,GAAc7gC,EAAU+lB,eAC3C,OAAO,GAAoBzE,KAAK4E,SAAU+b,EAAK3gB,KAAKyS,iBAAiB8P,GACvE,CACA,UAAA1P,GACE,MAAM,OACJ7qB,GACEgY,KAAK6E,QACT,MAAsB,iBAAX7c,EACFA,EAAO9F,MAAM,KAAKY,KAAInF,GAAS4f,OAAO6P,SAASzvB,EAAO,MAEzC,mBAAXqK,EACF8qB,GAAc9qB,EAAO8qB,EAAY9S,KAAK4E,UAExC5c,CACT,CACA,wBAAAy1B,CAAyBU,GACvB,OAAOthB,GAAQshB,EAAK,CAACne,KAAK4E,UAC5B,CACA,gBAAA6N,CAAiB8P,GACf,MAAMxP,EAAwB,CAC5Br0B,UAAW6jC,EACXnsB,UAAW,CAAC,CACV9V,KAAM,OACNmB,QAAS,CACPuO,mBAAoBgQ,KAAK6E,QAAQ7U,qBAElC,CACD1P,KAAM,SACNmB,QAAS,CACPuG,OAAQgY,KAAK6S,eAEd,CACDvyB,KAAM,kBACNmB,QAAS,CACPwM,SAAU+R,KAAK6E,QAAQ5W,WAExB,CACD3N,KAAM,QACNmB,QAAS,CACPlC,QAAS,IAAIygB,KAAKmE,YAAY5H,eAE/B,CACDjc,KAAM,kBACNC,SAAS,EACTC,MAAO,aACPC,GAAI4J,IAGF2V,KAAKwhB,iBAAiBpgC,aAAa,wBAAyBiJ,EAAK1J,MAAMjC,UAAU,KAIvF,MAAO,IACFq0B,KACAlW,GAAQmD,KAAK6E,QAAQgN,aAAc,CAACkB,IAE3C,CACA,aAAA6N,GACE,MAAM4B,EAAWxiB,KAAK6E,QAAQjD,QAAQ1f,MAAM,KAC5C,IAAK,MAAM0f,KAAW4gB,EACpB,GAAgB,UAAZ5gB,EACFrB,GAAac,GAAGrB,KAAK4E,SAAU5E,KAAKmE,YAAYqB,UAjVlC,SAiV4DxF,KAAK6E,QAAQ9K,UAAUqF,IAC/EY,KAAKqiB,6BAA6BjjB,GAC1CsI,QAAQ,SAEb,GA3VU,WA2VN9F,EAA4B,CACrC,MAAM6gB,EAAU7gB,IAAYyd,GAAgBrf,KAAKmE,YAAYqB,UAnV5C,cAmV0ExF,KAAKmE,YAAYqB,UArV5F,WAsVVkd,EAAW9gB,IAAYyd,GAAgBrf,KAAKmE,YAAYqB,UAnV7C,cAmV2ExF,KAAKmE,YAAYqB,UArV5F,YAsVjBjF,GAAac,GAAGrB,KAAK4E,SAAU6d,EAASziB,KAAK6E,QAAQ9K,UAAUqF,IAC7D,MAAM+T,EAAUnT,KAAKqiB,6BAA6BjjB,GAClD+T,EAAQqN,eAA8B,YAAfphB,EAAMqB,KAAqB6e,GAAgBD,KAAiB,EACnFlM,EAAQgO,QAAQ,IAElB5gB,GAAac,GAAGrB,KAAK4E,SAAU8d,EAAU1iB,KAAK6E,QAAQ9K,UAAUqF,IAC9D,MAAM+T,EAAUnT,KAAKqiB,6BAA6BjjB,GAClD+T,EAAQqN,eAA8B,aAAfphB,EAAMqB,KAAsB6e,GAAgBD,IAAiBlM,EAAQvO,SAASpgB,SAAS4a,EAAMU,eACpHqT,EAAQ+N,QAAQ,GAEpB,CAEFlhB,KAAKohB,kBAAoB,KACnBphB,KAAK4E,UACP5E,KAAKyP,MACP,EAEFlP,GAAac,GAAGrB,KAAK4E,SAAS5J,QAAQmkB,IAAiBC,GAAkBpf,KAAKohB,kBAChF,CACA,SAAAP,GACE,MAAMX,EAAQlgB,KAAK4E,SAASpJ,aAAa,SACpC0kB,IAGAlgB,KAAK4E,SAASpJ,aAAa,eAAkBwE,KAAK4E,SAAS0Z,YAAY3Y,QAC1E3F,KAAK4E,SAASxjB,aAAa,aAAc8+B,GAE3ClgB,KAAK4E,SAASxjB,aAAa,yBAA0B8+B,GACrDlgB,KAAK4E,SAASzjB,gBAAgB,SAChC,CACA,MAAAggC,GACMnhB,KAAKwP,YAAcxP,KAAKugB,WAC1BvgB,KAAKugB,YAAa,GAGpBvgB,KAAKugB,YAAa,EAClBvgB,KAAK2iB,aAAY,KACX3iB,KAAKugB,YACPvgB,KAAK0P,MACP,GACC1P,KAAK6E,QAAQob,MAAMvQ,MACxB,CACA,MAAAwR,GACMlhB,KAAKyhB,yBAGTzhB,KAAKugB,YAAa,EAClBvgB,KAAK2iB,aAAY,KACV3iB,KAAKugB,YACRvgB,KAAKyP,MACP,GACCzP,KAAK6E,QAAQob,MAAMxQ,MACxB,CACA,WAAAkT,CAAY/kB,EAASglB,GACnB7V,aAAa/M,KAAKsgB,UAClBtgB,KAAKsgB,SAAWziB,WAAWD,EAASglB,EACtC,CACA,oBAAAnB,GACE,OAAOzkC,OAAOmiB,OAAOa,KAAKwgB,gBAAgBpf,UAAS,EACrD,CACA,UAAAyC,CAAWC,GACT,MAAM+e,EAAiB7f,GAAYG,kBAAkBnD,KAAK4E,UAC1D,IAAK,MAAMke,KAAiB9lC,OAAO4D,KAAKiiC,GAClC7D,GAAsBroB,IAAImsB,WACrBD,EAAeC,GAU1B,OAPAhf,EAAS,IACJ+e,KACmB,iBAAX/e,GAAuBA,EAASA,EAAS,CAAC,GAEvDA,EAAS9D,KAAK+D,gBAAgBD,GAC9BA,EAAS9D,KAAKgE,kBAAkBF,GAChC9D,KAAKiE,iBAAiBH,GACfA,CACT,CACA,iBAAAE,CAAkBF,GAchB,OAbAA,EAAOic,WAAiC,IAArBjc,EAAOic,UAAsB16B,SAAS6G,KAAOwO,GAAWoJ,EAAOic,WACtD,iBAAjBjc,EAAOmc,QAChBnc,EAAOmc,MAAQ,CACbvQ,KAAM5L,EAAOmc,MACbxQ,KAAM3L,EAAOmc,QAGW,iBAAjBnc,EAAOoc,QAChBpc,EAAOoc,MAAQpc,EAAOoc,MAAMrgC,YAEA,iBAAnBikB,EAAOiZ,UAChBjZ,EAAOiZ,QAAUjZ,EAAOiZ,QAAQl9B,YAE3BikB,CACT,CACA,kBAAAwe,GACE,MAAMxe,EAAS,CAAC,EAChB,IAAK,MAAOhnB,EAAKa,KAAUX,OAAOmkB,QAAQnB,KAAK6E,SACzC7E,KAAKmE,YAAYT,QAAQ5mB,KAASa,IACpCmmB,EAAOhnB,GAAOa,GASlB,OANAmmB,EAAO/J,UAAW,EAClB+J,EAAOlC,QAAU,SAKVkC,CACT,CACA,cAAAud,GACMrhB,KAAKgS,UACPhS,KAAKgS,QAAQhZ,UACbgH,KAAKgS,QAAU,MAEbhS,KAAK2gB,MACP3gB,KAAK2gB,IAAIhnB,SACTqG,KAAK2gB,IAAM,KAEf,CAGA,sBAAOlkB,CAAgBqH,GACrB,OAAO9D,KAAKuH,MAAK,WACf,MAAMld,EAAO+1B,GAAQ9a,oBAAoBtF,KAAM8D,GAC/C,GAAsB,iBAAXA,EAAX,CAGA,QAA4B,IAAjBzZ,EAAKyZ,GACd,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,IAJL,CAKF,GACF,EAOF3H,GAAmBikB,IAcnB,MAGM2C,GAAY,IACb3C,GAAQ1c,QACXqZ,QAAS,GACT/0B,OAAQ,CAAC,EAAG,GACZtJ,UAAW,QACXy+B,SAAU,8IACVvb,QAAS,SAELohB,GAAgB,IACjB5C,GAAQzc,YACXoZ,QAAS,kCAOX,MAAMkG,WAAgB7C,GAEpB,kBAAW1c,GACT,OAAOqf,EACT,CACA,sBAAWpf,GACT,OAAOqf,EACT,CACA,eAAWzmB,GACT,MA7BW,SA8Bb,CAGA,cAAA+kB,GACE,OAAOthB,KAAK0hB,aAAe1hB,KAAKkjB,aAClC,CAGA,sBAAAtB,GACE,MAAO,CACL,kBAAkB5hB,KAAK0hB,YACvB,gBAAoB1hB,KAAKkjB,cAE7B,CACA,WAAAA,GACE,OAAOljB,KAAKyd,yBAAyBzd,KAAK6E,QAAQkY,QACpD,CAGA,sBAAOtgB,CAAgBqH,GACrB,OAAO9D,KAAKuH,MAAK,WACf,MAAMld,EAAO44B,GAAQ3d,oBAAoBtF,KAAM8D,GAC/C,GAAsB,iBAAXA,EAAX,CAGA,QAA4B,IAAjBzZ,EAAKyZ,GACd,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,IAJL,CAKF,GACF,EAOF3H,GAAmB8mB,IAcnB,MAEME,GAAc,gBAEdC,GAAiB,WAAWD,KAC5BE,GAAc,QAAQF,KACtBG,GAAwB,OAAOH,cAE/BI,GAAsB,SAEtBC,GAAwB,SAExBC,GAAqB,YAGrBC,GAAsB,GAAGD,mBAA+CA,uBAGxEE,GAAY,CAChB37B,OAAQ,KAER47B,WAAY,eACZC,cAAc,EACdt3B,OAAQ,KACRu3B,UAAW,CAAC,GAAK,GAAK,IAElBC,GAAgB,CACpB/7B,OAAQ,gBAER47B,WAAY,SACZC,aAAc,UACdt3B,OAAQ,UACRu3B,UAAW,SAOb,MAAME,WAAkBtf,GACtB,WAAAP,CAAY5kB,EAASukB,GACnBa,MAAMplB,EAASukB,GAGf9D,KAAKikB,aAAe,IAAI/yB,IACxB8O,KAAKkkB,oBAAsB,IAAIhzB,IAC/B8O,KAAKmkB,aAA6D,YAA9Cl/B,iBAAiB+a,KAAK4E,UAAU5Y,UAA0B,KAAOgU,KAAK4E,SAC1F5E,KAAKokB,cAAgB,KACrBpkB,KAAKqkB,UAAY,KACjBrkB,KAAKskB,oBAAsB,CACzBC,gBAAiB,EACjBC,gBAAiB,GAEnBxkB,KAAKykB,SACP,CAGA,kBAAW/gB,GACT,OAAOigB,EACT,CACA,sBAAWhgB,GACT,OAAOogB,EACT,CACA,eAAWxnB,GACT,MAhEW,WAiEb,CAGA,OAAAkoB,GACEzkB,KAAK0kB,mCACL1kB,KAAK2kB,2BACD3kB,KAAKqkB,UACPrkB,KAAKqkB,UAAUO,aAEf5kB,KAAKqkB,UAAYrkB,KAAK6kB,kBAExB,IAAK,MAAMC,KAAW9kB,KAAKkkB,oBAAoB/kB,SAC7Ca,KAAKqkB,UAAUU,QAAQD,EAE3B,CACA,OAAA/f,GACE/E,KAAKqkB,UAAUO,aACfjgB,MAAMI,SACR,CAGA,iBAAAf,CAAkBF,GAShB,OAPAA,EAAOvX,OAASmO,GAAWoJ,EAAOvX,SAAWlH,SAAS6G,KAGtD4X,EAAO8f,WAAa9f,EAAO9b,OAAS,GAAG8b,EAAO9b,oBAAsB8b,EAAO8f,WAC3C,iBAArB9f,EAAOggB,YAChBhgB,EAAOggB,UAAYhgB,EAAOggB,UAAU5hC,MAAM,KAAKY,KAAInF,GAAS4f,OAAOC,WAAW7f,MAEzEmmB,CACT,CACA,wBAAA6gB,GACO3kB,KAAK6E,QAAQgf,eAKlBtjB,GAAaC,IAAIR,KAAK6E,QAAQtY,OAAQ82B,IACtC9iB,GAAac,GAAGrB,KAAK6E,QAAQtY,OAAQ82B,GAAaG,IAAuBpkB,IACvE,MAAM4lB,EAAoBhlB,KAAKkkB,oBAAoB/mC,IAAIiiB,EAAM7S,OAAOtB,MACpE,GAAI+5B,EAAmB,CACrB5lB,EAAMkD,iBACN,MAAM3G,EAAOqE,KAAKmkB,cAAgBvkC,OAC5BmE,EAASihC,EAAkB3gC,UAAY2b,KAAK4E,SAASvgB,UAC3D,GAAIsX,EAAKspB,SAKP,YAJAtpB,EAAKspB,SAAS,CACZtjC,IAAKoC,EACLmhC,SAAU,WAMdvpB,EAAKlQ,UAAY1H,CACnB,KAEJ,CACA,eAAA8gC,GACE,MAAMpjC,EAAU,CACdka,KAAMqE,KAAKmkB,aACXL,UAAW9jB,KAAK6E,QAAQif,UACxBF,WAAY5jB,KAAK6E,QAAQ+e,YAE3B,OAAO,IAAIuB,sBAAqBhkB,GAAWnB,KAAKolB,kBAAkBjkB,IAAU1f,EAC9E,CAGA,iBAAA2jC,CAAkBjkB,GAChB,MAAMkkB,EAAgB/H,GAAStd,KAAKikB,aAAa9mC,IAAI,IAAImgC,EAAM/wB,OAAO4N,MAChEob,EAAW+H,IACftd,KAAKskB,oBAAoBC,gBAAkBjH,EAAM/wB,OAAOlI,UACxD2b,KAAKslB,SAASD,EAAc/H,GAAO,EAE/BkH,GAAmBxkB,KAAKmkB,cAAgB9+B,SAASC,iBAAiBmG,UAClE85B,EAAkBf,GAAmBxkB,KAAKskB,oBAAoBE,gBACpExkB,KAAKskB,oBAAoBE,gBAAkBA,EAC3C,IAAK,MAAMlH,KAASnc,EAAS,CAC3B,IAAKmc,EAAMkI,eAAgB,CACzBxlB,KAAKokB,cAAgB,KACrBpkB,KAAKylB,kBAAkBJ,EAAc/H,IACrC,QACF,CACA,MAAMoI,EAA2BpI,EAAM/wB,OAAOlI,WAAa2b,KAAKskB,oBAAoBC,gBAEpF,GAAIgB,GAAmBG,GAGrB,GAFAnQ,EAAS+H,IAEJkH,EACH,YAMCe,GAAoBG,GACvBnQ,EAAS+H,EAEb,CACF,CACA,gCAAAoH,GACE1kB,KAAKikB,aAAe,IAAI/yB,IACxB8O,KAAKkkB,oBAAsB,IAAIhzB,IAC/B,MAAMy0B,EAAc/f,GAAezT,KAAKqxB,GAAuBxjB,KAAK6E,QAAQtY,QAC5E,IAAK,MAAMq5B,KAAUD,EAAa,CAEhC,IAAKC,EAAO36B,MAAQiQ,GAAW0qB,GAC7B,SAEF,MAAMZ,EAAoBpf,GAAeC,QAAQggB,UAAUD,EAAO36B,MAAO+U,KAAK4E,UAG1EjK,GAAUqqB,KACZhlB,KAAKikB,aAAalyB,IAAI8zB,UAAUD,EAAO36B,MAAO26B,GAC9C5lB,KAAKkkB,oBAAoBnyB,IAAI6zB,EAAO36B,KAAM+5B,GAE9C,CACF,CACA,QAAAM,CAAS/4B,GACHyT,KAAKokB,gBAAkB73B,IAG3ByT,KAAKylB,kBAAkBzlB,KAAK6E,QAAQtY,QACpCyT,KAAKokB,cAAgB73B,EACrBA,EAAO8O,UAAU5E,IAAI8sB,IACrBvjB,KAAK8lB,iBAAiBv5B,GACtBgU,GAAaqB,QAAQ5B,KAAK4E,SAAUwe,GAAgB,CAClDtjB,cAAevT,IAEnB,CACA,gBAAAu5B,CAAiBv5B,GAEf,GAAIA,EAAO8O,UAAU7W,SA9LQ,iBA+L3BohB,GAAeC,QArLc,mBAqLsBtZ,EAAOyO,QAtLtC,cAsLkEK,UAAU5E,IAAI8sB,SAGtG,IAAK,MAAMwC,KAAangB,GAAeI,QAAQzZ,EA9LnB,qBAiM1B,IAAK,MAAMxJ,KAAQ6iB,GAAeM,KAAK6f,EAAWrC,IAChD3gC,EAAKsY,UAAU5E,IAAI8sB,GAGzB,CACA,iBAAAkC,CAAkBhhC,GAChBA,EAAO4W,UAAU1B,OAAO4pB,IACxB,MAAMyC,EAAcpgB,GAAezT,KAAK,GAAGqxB,MAAyBD,KAAuB9+B,GAC3F,IAAK,MAAM9E,KAAQqmC,EACjBrmC,EAAK0b,UAAU1B,OAAO4pB,GAE1B,CAGA,sBAAO9mB,CAAgBqH,GACrB,OAAO9D,KAAKuH,MAAK,WACf,MAAMld,EAAO25B,GAAU1e,oBAAoBtF,KAAM8D,GACjD,GAAsB,iBAAXA,EAAX,CAGA,QAAqB/K,IAAjB1O,EAAKyZ,IAAyBA,EAAOrC,WAAW,MAAmB,gBAAXqC,EAC1D,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,IAJL,CAKF,GACF,EAOFvD,GAAac,GAAGzhB,OAAQ0jC,IAAuB,KAC7C,IAAK,MAAM2C,KAAOrgB,GAAezT,KApOT,0BAqOtB6xB,GAAU1e,oBAAoB2gB,EAChC,IAOF9pB,GAAmB6nB,IAcnB,MAEMkC,GAAc,UACdC,GAAe,OAAOD,KACtBE,GAAiB,SAASF,KAC1BG,GAAe,OAAOH,KACtBI,GAAgB,QAAQJ,KACxBK,GAAuB,QAAQL,KAC/BM,GAAgB,UAAUN,KAC1BO,GAAsB,OAAOP,KAC7BQ,GAAiB,YACjBC,GAAkB,aAClBC,GAAe,UACfC,GAAiB,YACjBC,GAAW,OACXC,GAAU,MACVC,GAAoB,SACpBC,GAAoB,OACpBC,GAAoB,OAEpBC,GAA2B,mBAE3BC,GAA+B,QAAQD,MAIvCE,GAAuB,2EACvBC,GAAsB,YAFOF,uBAAiDA,mBAA6CA,OAE/EC,KAC5CE,GAA8B,IAAIP,8BAA6CA,+BAA8CA,4BAMnI,MAAMQ,WAAY9iB,GAChB,WAAAP,CAAY5kB,GACVolB,MAAMplB,GACNygB,KAAKiS,QAAUjS,KAAK4E,SAAS5J,QAdN,uCAelBgF,KAAKiS,UAOVjS,KAAKynB,sBAAsBznB,KAAKiS,QAASjS,KAAK0nB,gBAC9CnnB,GAAac,GAAGrB,KAAK4E,SAAU4hB,IAAepnB,GAASY,KAAK0M,SAAStN,KACvE,CAGA,eAAW7C,GACT,MAnDW,KAoDb,CAGA,IAAAmT,GAEE,MAAMiY,EAAY3nB,KAAK4E,SACvB,GAAI5E,KAAK4nB,cAAcD,GACrB,OAIF,MAAME,EAAS7nB,KAAK8nB,iBACdC,EAAYF,EAAStnB,GAAaqB,QAAQimB,EAAQ1B,GAAc,CACpErmB,cAAe6nB,IACZ,KACapnB,GAAaqB,QAAQ+lB,EAAWtB,GAAc,CAC9DvmB,cAAe+nB,IAEH7lB,kBAAoB+lB,GAAaA,EAAU/lB,mBAGzDhC,KAAKgoB,YAAYH,EAAQF,GACzB3nB,KAAKioB,UAAUN,EAAWE,GAC5B,CAGA,SAAAI,CAAU1oC,EAAS2oC,GACZ3oC,IAGLA,EAAQ8b,UAAU5E,IAAIuwB,IACtBhnB,KAAKioB,UAAUriB,GAAec,uBAAuBnnB,IAcrDygB,KAAKmF,gBAZY,KACsB,QAAjC5lB,EAAQic,aAAa,SAIzBjc,EAAQ4B,gBAAgB,YACxB5B,EAAQ6B,aAAa,iBAAiB,GACtC4e,KAAKmoB,gBAAgB5oC,GAAS,GAC9BghB,GAAaqB,QAAQriB,EAAS+mC,GAAe,CAC3CxmB,cAAeooB,KAPf3oC,EAAQ8b,UAAU5E,IAAIywB,GAQtB,GAE0B3nC,EAASA,EAAQ8b,UAAU7W,SAASyiC,KACpE,CACA,WAAAe,CAAYzoC,EAAS2oC,GACd3oC,IAGLA,EAAQ8b,UAAU1B,OAAOqtB,IACzBznC,EAAQm7B,OACR1a,KAAKgoB,YAAYpiB,GAAec,uBAAuBnnB,IAcvDygB,KAAKmF,gBAZY,KACsB,QAAjC5lB,EAAQic,aAAa,SAIzBjc,EAAQ6B,aAAa,iBAAiB,GACtC7B,EAAQ6B,aAAa,WAAY,MACjC4e,KAAKmoB,gBAAgB5oC,GAAS,GAC9BghB,GAAaqB,QAAQriB,EAAS6mC,GAAgB,CAC5CtmB,cAAeooB,KAPf3oC,EAAQ8b,UAAU1B,OAAOutB,GAQzB,GAE0B3nC,EAASA,EAAQ8b,UAAU7W,SAASyiC,KACpE,CACA,QAAAva,CAAStN,GACP,IAAK,CAACsnB,GAAgBC,GAAiBC,GAAcC,GAAgBC,GAAUC,IAAS3lB,SAAShC,EAAMtiB,KACrG,OAEFsiB,EAAMuU,kBACNvU,EAAMkD,iBACN,MAAMwD,EAAW9F,KAAK0nB,eAAevhC,QAAO5G,IAAY2b,GAAW3b,KACnE,IAAI6oC,EACJ,GAAI,CAACtB,GAAUC,IAAS3lB,SAAShC,EAAMtiB,KACrCsrC,EAAoBtiB,EAAS1G,EAAMtiB,MAAQgqC,GAAW,EAAIhhB,EAASpV,OAAS,OACvE,CACL,MAAM2c,EAAS,CAACsZ,GAAiBE,IAAgBzlB,SAAShC,EAAMtiB,KAChEsrC,EAAoBtqB,GAAqBgI,EAAU1G,EAAM7S,OAAQ8gB,GAAQ,EAC3E,CACI+a,IACFA,EAAkB9V,MAAM,CACtB+V,eAAe,IAEjBb,GAAIliB,oBAAoB8iB,GAAmB1Y,OAE/C,CACA,YAAAgY,GAEE,OAAO9hB,GAAezT,KAAKm1B,GAAqBtnB,KAAKiS,QACvD,CACA,cAAA6V,GACE,OAAO9nB,KAAK0nB,eAAev1B,MAAKzN,GAASsb,KAAK4nB,cAAcljC,MAAW,IACzE,CACA,qBAAA+iC,CAAsBhjC,EAAQqhB,GAC5B9F,KAAKsoB,yBAAyB7jC,EAAQ,OAAQ,WAC9C,IAAK,MAAMC,KAASohB,EAClB9F,KAAKuoB,6BAA6B7jC,EAEtC,CACA,4BAAA6jC,CAA6B7jC,GAC3BA,EAAQsb,KAAKwoB,iBAAiB9jC,GAC9B,MAAM+jC,EAAWzoB,KAAK4nB,cAAcljC,GAC9BgkC,EAAY1oB,KAAK2oB,iBAAiBjkC,GACxCA,EAAMtD,aAAa,gBAAiBqnC,GAChCC,IAAchkC,GAChBsb,KAAKsoB,yBAAyBI,EAAW,OAAQ,gBAE9CD,GACH/jC,EAAMtD,aAAa,WAAY,MAEjC4e,KAAKsoB,yBAAyB5jC,EAAO,OAAQ,OAG7Csb,KAAK4oB,mCAAmClkC,EAC1C,CACA,kCAAAkkC,CAAmClkC,GACjC,MAAM6H,EAASqZ,GAAec,uBAAuBhiB,GAChD6H,IAGLyT,KAAKsoB,yBAAyB/7B,EAAQ,OAAQ,YAC1C7H,EAAMyV,IACR6F,KAAKsoB,yBAAyB/7B,EAAQ,kBAAmB,GAAG7H,EAAMyV,MAEtE,CACA,eAAAguB,CAAgB5oC,EAASspC,GACvB,MAAMH,EAAY1oB,KAAK2oB,iBAAiBppC,GACxC,IAAKmpC,EAAUrtB,UAAU7W,SApKN,YAqKjB,OAEF,MAAMkjB,EAAS,CAAC3N,EAAUia,KACxB,MAAMz0B,EAAUqmB,GAAeC,QAAQ9L,EAAU2uB,GAC7CnpC,GACFA,EAAQ8b,UAAUqM,OAAOsM,EAAW6U,EACtC,EAEFnhB,EAAOyf,GAA0BH,IACjCtf,EA5K2B,iBA4KIwf,IAC/BwB,EAAUtnC,aAAa,gBAAiBynC,EAC1C,CACA,wBAAAP,CAAyB/oC,EAASwC,EAAWpE,GACtC4B,EAAQgc,aAAaxZ,IACxBxC,EAAQ6B,aAAaW,EAAWpE,EAEpC,CACA,aAAAiqC,CAAczY,GACZ,OAAOA,EAAK9T,UAAU7W,SAASwiC,GACjC,CAGA,gBAAAwB,CAAiBrZ,GACf,OAAOA,EAAKpJ,QAAQuhB,IAAuBnY,EAAOvJ,GAAeC,QAAQyhB,GAAqBnY,EAChG,CAGA,gBAAAwZ,CAAiBxZ,GACf,OAAOA,EAAKnU,QA5LO,gCA4LoBmU,CACzC,CAGA,sBAAO1S,CAAgBqH,GACrB,OAAO9D,KAAKuH,MAAK,WACf,MAAMld,EAAOm9B,GAAIliB,oBAAoBtF,MACrC,GAAsB,iBAAX8D,EAAX,CAGA,QAAqB/K,IAAjB1O,EAAKyZ,IAAyBA,EAAOrC,WAAW,MAAmB,gBAAXqC,EAC1D,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,IAJL,CAKF,GACF,EAOFvD,GAAac,GAAGhc,SAAUkhC,GAAsBc,IAAsB,SAAUjoB,GAC1E,CAAC,IAAK,QAAQgC,SAASpB,KAAKgH,UAC9B5H,EAAMkD,iBAEJpH,GAAW8E,OAGfwnB,GAAIliB,oBAAoBtF,MAAM0P,MAChC,IAKAnP,GAAac,GAAGzhB,OAAQ6mC,IAAqB,KAC3C,IAAK,MAAMlnC,KAAWqmB,GAAezT,KAAKo1B,IACxCC,GAAIliB,oBAAoB/lB,EAC1B,IAMF4c,GAAmBqrB,IAcnB,MAEMxiB,GAAY,YACZ8jB,GAAkB,YAAY9jB,KAC9B+jB,GAAiB,WAAW/jB,KAC5BgkB,GAAgB,UAAUhkB,KAC1BikB,GAAiB,WAAWjkB,KAC5BkkB,GAAa,OAAOlkB,KACpBmkB,GAAe,SAASnkB,KACxBokB,GAAa,OAAOpkB,KACpBqkB,GAAc,QAAQrkB,KAEtBskB,GAAkB,OAClBC,GAAkB,OAClBC,GAAqB,UACrB7lB,GAAc,CAClBmc,UAAW,UACX2J,SAAU,UACVxJ,MAAO,UAEHvc,GAAU,CACdoc,WAAW,EACX2J,UAAU,EACVxJ,MAAO,KAOT,MAAMyJ,WAAchlB,GAClB,WAAAP,CAAY5kB,EAASukB,GACnBa,MAAMplB,EAASukB,GACf9D,KAAKsgB,SAAW,KAChBtgB,KAAK2pB,sBAAuB,EAC5B3pB,KAAK4pB,yBAA0B,EAC/B5pB,KAAK4gB,eACP,CAGA,kBAAWld,GACT,OAAOA,EACT,CACA,sBAAWC,GACT,OAAOA,EACT,CACA,eAAWpH,GACT,MA/CS,OAgDX,CAGA,IAAAmT,GACoBnP,GAAaqB,QAAQ5B,KAAK4E,SAAUwkB,IACxCpnB,mBAGdhC,KAAK6pB,gBACD7pB,KAAK6E,QAAQib,WACf9f,KAAK4E,SAASvJ,UAAU5E,IA/CN,QAsDpBuJ,KAAK4E,SAASvJ,UAAU1B,OAAO2vB,IAC/BztB,GAAOmE,KAAK4E,UACZ5E,KAAK4E,SAASvJ,UAAU5E,IAAI8yB,GAAiBC,IAC7CxpB,KAAKmF,gBARY,KACfnF,KAAK4E,SAASvJ,UAAU1B,OAAO6vB,IAC/BjpB,GAAaqB,QAAQ5B,KAAK4E,SAAUykB,IACpCrpB,KAAK8pB,oBAAoB,GAKG9pB,KAAK4E,SAAU5E,KAAK6E,QAAQib,WAC5D,CACA,IAAArQ,GACOzP,KAAK+pB,YAGQxpB,GAAaqB,QAAQ5B,KAAK4E,SAAUskB,IACxClnB,mBAQdhC,KAAK4E,SAASvJ,UAAU5E,IAAI+yB,IAC5BxpB,KAAKmF,gBANY,KACfnF,KAAK4E,SAASvJ,UAAU5E,IAAI6yB,IAC5BtpB,KAAK4E,SAASvJ,UAAU1B,OAAO6vB,GAAoBD,IACnDhpB,GAAaqB,QAAQ5B,KAAK4E,SAAUukB,GAAa,GAGrBnpB,KAAK4E,SAAU5E,KAAK6E,QAAQib,YAC5D,CACA,OAAA/a,GACE/E,KAAK6pB,gBACD7pB,KAAK+pB,WACP/pB,KAAK4E,SAASvJ,UAAU1B,OAAO4vB,IAEjC5kB,MAAMI,SACR,CACA,OAAAglB,GACE,OAAO/pB,KAAK4E,SAASvJ,UAAU7W,SAAS+kC,GAC1C,CAIA,kBAAAO,GACO9pB,KAAK6E,QAAQ4kB,WAGdzpB,KAAK2pB,sBAAwB3pB,KAAK4pB,0BAGtC5pB,KAAKsgB,SAAWziB,YAAW,KACzBmC,KAAKyP,MAAM,GACVzP,KAAK6E,QAAQob,QAClB,CACA,cAAA+J,CAAe5qB,EAAO6qB,GACpB,OAAQ7qB,EAAMqB,MACZ,IAAK,YACL,IAAK,WAEDT,KAAK2pB,qBAAuBM,EAC5B,MAEJ,IAAK,UACL,IAAK,WAEDjqB,KAAK4pB,wBAA0BK,EAIrC,GAAIA,EAEF,YADAjqB,KAAK6pB,gBAGP,MAAMvc,EAAclO,EAAMU,cACtBE,KAAK4E,WAAa0I,GAAetN,KAAK4E,SAASpgB,SAAS8oB,IAG5DtN,KAAK8pB,oBACP,CACA,aAAAlJ,GACErgB,GAAac,GAAGrB,KAAK4E,SAAUkkB,IAAiB1pB,GAASY,KAAKgqB,eAAe5qB,GAAO,KACpFmB,GAAac,GAAGrB,KAAK4E,SAAUmkB,IAAgB3pB,GAASY,KAAKgqB,eAAe5qB,GAAO,KACnFmB,GAAac,GAAGrB,KAAK4E,SAAUokB,IAAe5pB,GAASY,KAAKgqB,eAAe5qB,GAAO,KAClFmB,GAAac,GAAGrB,KAAK4E,SAAUqkB,IAAgB7pB,GAASY,KAAKgqB,eAAe5qB,GAAO,IACrF,CACA,aAAAyqB,GACE9c,aAAa/M,KAAKsgB,UAClBtgB,KAAKsgB,SAAW,IAClB,CAGA,sBAAO7jB,CAAgBqH,GACrB,OAAO9D,KAAKuH,MAAK,WACf,MAAMld,EAAOq/B,GAAMpkB,oBAAoBtF,KAAM8D,GAC7C,GAAsB,iBAAXA,EAAqB,CAC9B,QAA4B,IAAjBzZ,EAAKyZ,GACd,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,GAAQ9D,KACf,CACF,GACF,ECr0IK,SAASkqB,GAAc7tB,GACD,WAAvBhX,SAASuX,WAAyBP,IACjChX,SAASyF,iBAAiB,mBAAoBuR,EACrD,CDy0IAuK,GAAqB8iB,IAMrBvtB,GAAmButB,IEpyInBQ,IAzCA,WAC2B,GAAG93B,MAAM5U,KAChC6H,SAAS+a,iBAAiB,+BAETtd,KAAI,SAAUqnC,GAC/B,OAAO,IAAI,GAAkBA,EAAkB,CAC7ClK,MAAO,CAAEvQ,KAAM,IAAKD,KAAM,MAE9B,GACF,IAiCAya,IA5BA,WACY7kC,SAAS68B,eAAe,mBAC9Bp3B,iBAAiB,SAAS,WAC5BzF,SAAS6G,KAAKT,UAAY,EAC1BpG,SAASC,gBAAgBmG,UAAY,CACvC,GACF,IAuBAy+B,IArBA,WACE,IAAIE,EAAM/kC,SAAS68B,eAAe,mBAC9BmI,EAAShlC,SACVilC,uBAAuB,aAAa,GACpChnC,wBACH1D,OAAOkL,iBAAiB,UAAU,WAC5BkV,KAAKuqB,UAAYvqB,KAAKwqB,SAAWxqB,KAAKwqB,QAAUH,EAAOzsC,OACzDwsC,EAAIrpC,MAAM6wB,QAAU,QAEpBwY,EAAIrpC,MAAM6wB,QAAU,OAEtB5R,KAAKuqB,UAAYvqB,KAAKwqB,OACxB,GACF,IAUA5qC,OAAO6qC,UAAY","sources":["webpack://pydata_sphinx_theme/webpack/bootstrap","webpack://pydata_sphinx_theme/webpack/runtime/define property getters","webpack://pydata_sphinx_theme/webpack/runtime/hasOwnProperty shorthand","webpack://pydata_sphinx_theme/webpack/runtime/make namespace object","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/enums.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getNodeName.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getWindow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/instanceOf.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/applyStyles.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getBasePlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/math.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/userAgent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/isLayoutViewport.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getBoundingClientRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getLayoutRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/contains.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getComputedStyle.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/isTableElement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getDocumentElement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getParentNode.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getOffsetParent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getMainAxisFromPlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/within.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/mergePaddingObject.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getFreshSideObject.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/expandToHashMap.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/arrow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getVariation.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/computeStyles.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/eventListeners.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getOppositePlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getOppositeVariationPlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getWindowScroll.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getWindowScrollBarX.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/isScrollParent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getScrollParent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/listScrollParents.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/rectToClientRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getClippingRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getViewportRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getDocumentRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/computeOffsets.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/detectOverflow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/flip.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/computeAutoPlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/hide.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/offset.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/popperOffsets.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/preventOverflow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getAltAxis.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getCompositeRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getNodeScroll.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getHTMLElementScroll.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/orderModifiers.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/createPopper.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/debounce.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/mergeByName.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/popper.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/popper-lite.js","webpack://pydata_sphinx_theme/./node_modules/bootstrap/dist/js/bootstrap.esm.js","webpack://pydata_sphinx_theme/./src/pydata_sphinx_theme/assets/scripts/mixin.js","webpack://pydata_sphinx_theme/./src/pydata_sphinx_theme/assets/scripts/bootstrap.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","export var top = 'top';\nexport var bottom = 'bottom';\nexport var right = 'right';\nexport var left = 'left';\nexport var auto = 'auto';\nexport var basePlacements = [top, bottom, right, left];\nexport var start = 'start';\nexport var end = 'end';\nexport var clippingParents = 'clippingParents';\nexport var viewport = 'viewport';\nexport var popper = 'popper';\nexport var reference = 'reference';\nexport var variationPlacements = /*#__PURE__*/basePlacements.reduce(function (acc, placement) {\n return acc.concat([placement + \"-\" + start, placement + \"-\" + end]);\n}, []);\nexport var placements = /*#__PURE__*/[].concat(basePlacements, [auto]).reduce(function (acc, placement) {\n return acc.concat([placement, placement + \"-\" + start, placement + \"-\" + end]);\n}, []); // modifiers that need to read the DOM\n\nexport var beforeRead = 'beforeRead';\nexport var read = 'read';\nexport var afterRead = 'afterRead'; // pure-logic modifiers\n\nexport var beforeMain = 'beforeMain';\nexport var main = 'main';\nexport var afterMain = 'afterMain'; // modifier with the purpose to write to the DOM (or write into a framework state)\n\nexport var beforeWrite = 'beforeWrite';\nexport var write = 'write';\nexport var afterWrite = 'afterWrite';\nexport var modifierPhases = [beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite];","export default function getNodeName(element) {\n return element ? (element.nodeName || '').toLowerCase() : null;\n}","export default function getWindow(node) {\n if (node == null) {\n return window;\n }\n\n if (node.toString() !== '[object Window]') {\n var ownerDocument = node.ownerDocument;\n return ownerDocument ? ownerDocument.defaultView || window : window;\n }\n\n return node;\n}","import getWindow from \"./getWindow.js\";\n\nfunction isElement(node) {\n var OwnElement = getWindow(node).Element;\n return node instanceof OwnElement || node instanceof Element;\n}\n\nfunction isHTMLElement(node) {\n var OwnElement = getWindow(node).HTMLElement;\n return node instanceof OwnElement || node instanceof HTMLElement;\n}\n\nfunction isShadowRoot(node) {\n // IE 11 has no ShadowRoot\n if (typeof ShadowRoot === 'undefined') {\n return false;\n }\n\n var OwnElement = getWindow(node).ShadowRoot;\n return node instanceof OwnElement || node instanceof ShadowRoot;\n}\n\nexport { isElement, isHTMLElement, isShadowRoot };","import getNodeName from \"../dom-utils/getNodeName.js\";\nimport { isHTMLElement } from \"../dom-utils/instanceOf.js\"; // This modifier takes the styles prepared by the `computeStyles` modifier\n// and applies them to the HTMLElements such as popper and arrow\n\nfunction applyStyles(_ref) {\n var state = _ref.state;\n Object.keys(state.elements).forEach(function (name) {\n var style = state.styles[name] || {};\n var attributes = state.attributes[name] || {};\n var element = state.elements[name]; // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n } // Flow doesn't support to extend this property, but it's the most\n // effective way to apply styles to an HTMLElement\n // $FlowFixMe[cannot-write]\n\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (name) {\n var value = attributes[name];\n\n if (value === false) {\n element.removeAttribute(name);\n } else {\n element.setAttribute(name, value === true ? '' : value);\n }\n });\n });\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state;\n var initialStyles = {\n popper: {\n position: state.options.strategy,\n left: '0',\n top: '0',\n margin: '0'\n },\n arrow: {\n position: 'absolute'\n },\n reference: {}\n };\n Object.assign(state.elements.popper.style, initialStyles.popper);\n state.styles = initialStyles;\n\n if (state.elements.arrow) {\n Object.assign(state.elements.arrow.style, initialStyles.arrow);\n }\n\n return function () {\n Object.keys(state.elements).forEach(function (name) {\n var element = state.elements[name];\n var attributes = state.attributes[name] || {};\n var styleProperties = Object.keys(state.styles.hasOwnProperty(name) ? state.styles[name] : initialStyles[name]); // Set all values to an empty string to unset them\n\n var style = styleProperties.reduce(function (style, property) {\n style[property] = '';\n return style;\n }, {}); // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n }\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (attribute) {\n element.removeAttribute(attribute);\n });\n });\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'applyStyles',\n enabled: true,\n phase: 'write',\n fn: applyStyles,\n effect: effect,\n requires: ['computeStyles']\n};","import { auto } from \"../enums.js\";\nexport default function getBasePlacement(placement) {\n return placement.split('-')[0];\n}","export var max = Math.max;\nexport var min = Math.min;\nexport var round = Math.round;","export default function getUAString() {\n var uaData = navigator.userAgentData;\n\n if (uaData != null && uaData.brands && Array.isArray(uaData.brands)) {\n return uaData.brands.map(function (item) {\n return item.brand + \"/\" + item.version;\n }).join(' ');\n }\n\n return navigator.userAgent;\n}","import getUAString from \"../utils/userAgent.js\";\nexport default function isLayoutViewport() {\n return !/^((?!chrome|android).)*safari/i.test(getUAString());\n}","import { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport { round } from \"../utils/math.js\";\nimport getWindow from \"./getWindow.js\";\nimport isLayoutViewport from \"./isLayoutViewport.js\";\nexport default function getBoundingClientRect(element, includeScale, isFixedStrategy) {\n if (includeScale === void 0) {\n includeScale = false;\n }\n\n if (isFixedStrategy === void 0) {\n isFixedStrategy = false;\n }\n\n var clientRect = element.getBoundingClientRect();\n var scaleX = 1;\n var scaleY = 1;\n\n if (includeScale && isHTMLElement(element)) {\n scaleX = element.offsetWidth > 0 ? round(clientRect.width) / element.offsetWidth || 1 : 1;\n scaleY = element.offsetHeight > 0 ? round(clientRect.height) / element.offsetHeight || 1 : 1;\n }\n\n var _ref = isElement(element) ? getWindow(element) : window,\n visualViewport = _ref.visualViewport;\n\n var addVisualOffsets = !isLayoutViewport() && isFixedStrategy;\n var x = (clientRect.left + (addVisualOffsets && visualViewport ? visualViewport.offsetLeft : 0)) / scaleX;\n var y = (clientRect.top + (addVisualOffsets && visualViewport ? visualViewport.offsetTop : 0)) / scaleY;\n var width = clientRect.width / scaleX;\n var height = clientRect.height / scaleY;\n return {\n width: width,\n height: height,\n top: y,\n right: x + width,\n bottom: y + height,\n left: x,\n x: x,\n y: y\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\"; // Returns the layout rect of an element relative to its offsetParent. Layout\n// means it doesn't take into account transforms.\n\nexport default function getLayoutRect(element) {\n var clientRect = getBoundingClientRect(element); // Use the clientRect sizes if it's not been transformed.\n // Fixes https://github.com/popperjs/popper-core/issues/1223\n\n var width = element.offsetWidth;\n var height = element.offsetHeight;\n\n if (Math.abs(clientRect.width - width) <= 1) {\n width = clientRect.width;\n }\n\n if (Math.abs(clientRect.height - height) <= 1) {\n height = clientRect.height;\n }\n\n return {\n x: element.offsetLeft,\n y: element.offsetTop,\n width: width,\n height: height\n };\n}","import { isShadowRoot } from \"./instanceOf.js\";\nexport default function contains(parent, child) {\n var rootNode = child.getRootNode && child.getRootNode(); // First, attempt with faster native method\n\n if (parent.contains(child)) {\n return true;\n } // then fallback to custom implementation with Shadow DOM support\n else if (rootNode && isShadowRoot(rootNode)) {\n var next = child;\n\n do {\n if (next && parent.isSameNode(next)) {\n return true;\n } // $FlowFixMe[prop-missing]: need a better way to handle this...\n\n\n next = next.parentNode || next.host;\n } while (next);\n } // Give up, the result is false\n\n\n return false;\n}","import getWindow from \"./getWindow.js\";\nexport default function getComputedStyle(element) {\n return getWindow(element).getComputedStyle(element);\n}","import getNodeName from \"./getNodeName.js\";\nexport default function isTableElement(element) {\n return ['table', 'td', 'th'].indexOf(getNodeName(element)) >= 0;\n}","import { isElement } from \"./instanceOf.js\";\nexport default function getDocumentElement(element) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return ((isElement(element) ? element.ownerDocument : // $FlowFixMe[prop-missing]\n element.document) || window.document).documentElement;\n}","import getNodeName from \"./getNodeName.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport { isShadowRoot } from \"./instanceOf.js\";\nexport default function getParentNode(element) {\n if (getNodeName(element) === 'html') {\n return element;\n }\n\n return (// this is a quicker (but less type safe) way to save quite some bytes from the bundle\n // $FlowFixMe[incompatible-return]\n // $FlowFixMe[prop-missing]\n element.assignedSlot || // step into the shadow DOM of the parent of a slotted node\n element.parentNode || ( // DOM Element detected\n isShadowRoot(element) ? element.host : null) || // ShadowRoot detected\n // $FlowFixMe[incompatible-call]: HTMLElement is a Node\n getDocumentElement(element) // fallback\n\n );\n}","import getWindow from \"./getWindow.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isHTMLElement, isShadowRoot } from \"./instanceOf.js\";\nimport isTableElement from \"./isTableElement.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getUAString from \"../utils/userAgent.js\";\n\nfunction getTrueOffsetParent(element) {\n if (!isHTMLElement(element) || // https://github.com/popperjs/popper-core/issues/837\n getComputedStyle(element).position === 'fixed') {\n return null;\n }\n\n return element.offsetParent;\n} // `.offsetParent` reports `null` for fixed elements, while absolute elements\n// return the containing block\n\n\nfunction getContainingBlock(element) {\n var isFirefox = /firefox/i.test(getUAString());\n var isIE = /Trident/i.test(getUAString());\n\n if (isIE && isHTMLElement(element)) {\n // In IE 9, 10 and 11 fixed elements containing block is always established by the viewport\n var elementCss = getComputedStyle(element);\n\n if (elementCss.position === 'fixed') {\n return null;\n }\n }\n\n var currentNode = getParentNode(element);\n\n if (isShadowRoot(currentNode)) {\n currentNode = currentNode.host;\n }\n\n while (isHTMLElement(currentNode) && ['html', 'body'].indexOf(getNodeName(currentNode)) < 0) {\n var css = getComputedStyle(currentNode); // This is non-exhaustive but covers the most common CSS properties that\n // create a containing block.\n // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\n\n if (css.transform !== 'none' || css.perspective !== 'none' || css.contain === 'paint' || ['transform', 'perspective'].indexOf(css.willChange) !== -1 || isFirefox && css.willChange === 'filter' || isFirefox && css.filter && css.filter !== 'none') {\n return currentNode;\n } else {\n currentNode = currentNode.parentNode;\n }\n }\n\n return null;\n} // Gets the closest ancestor positioned element. Handles some edge cases,\n// such as table ancestors and cross browser bugs.\n\n\nexport default function getOffsetParent(element) {\n var window = getWindow(element);\n var offsetParent = getTrueOffsetParent(element);\n\n while (offsetParent && isTableElement(offsetParent) && getComputedStyle(offsetParent).position === 'static') {\n offsetParent = getTrueOffsetParent(offsetParent);\n }\n\n if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle(offsetParent).position === 'static')) {\n return window;\n }\n\n return offsetParent || getContainingBlock(element) || window;\n}","export default function getMainAxisFromPlacement(placement) {\n return ['top', 'bottom'].indexOf(placement) >= 0 ? 'x' : 'y';\n}","import { max as mathMax, min as mathMin } from \"./math.js\";\nexport function within(min, value, max) {\n return mathMax(min, mathMin(value, max));\n}\nexport function withinMaxClamp(min, value, max) {\n var v = within(min, value, max);\n return v > max ? max : v;\n}","import getFreshSideObject from \"./getFreshSideObject.js\";\nexport default function mergePaddingObject(paddingObject) {\n return Object.assign({}, getFreshSideObject(), paddingObject);\n}","export default function getFreshSideObject() {\n return {\n top: 0,\n right: 0,\n bottom: 0,\n left: 0\n };\n}","export default function expandToHashMap(value, keys) {\n return keys.reduce(function (hashMap, key) {\n hashMap[key] = value;\n return hashMap;\n }, {});\n}","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport contains from \"../dom-utils/contains.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport { within } from \"../utils/within.js\";\nimport mergePaddingObject from \"../utils/mergePaddingObject.js\";\nimport expandToHashMap from \"../utils/expandToHashMap.js\";\nimport { left, right, basePlacements, top, bottom } from \"../enums.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar toPaddingObject = function toPaddingObject(padding, state) {\n padding = typeof padding === 'function' ? padding(Object.assign({}, state.rects, {\n placement: state.placement\n })) : padding;\n return mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n};\n\nfunction arrow(_ref) {\n var _state$modifiersData$;\n\n var state = _ref.state,\n name = _ref.name,\n options = _ref.options;\n var arrowElement = state.elements.arrow;\n var popperOffsets = state.modifiersData.popperOffsets;\n var basePlacement = getBasePlacement(state.placement);\n var axis = getMainAxisFromPlacement(basePlacement);\n var isVertical = [left, right].indexOf(basePlacement) >= 0;\n var len = isVertical ? 'height' : 'width';\n\n if (!arrowElement || !popperOffsets) {\n return;\n }\n\n var paddingObject = toPaddingObject(options.padding, state);\n var arrowRect = getLayoutRect(arrowElement);\n var minProp = axis === 'y' ? top : left;\n var maxProp = axis === 'y' ? bottom : right;\n var endDiff = state.rects.reference[len] + state.rects.reference[axis] - popperOffsets[axis] - state.rects.popper[len];\n var startDiff = popperOffsets[axis] - state.rects.reference[axis];\n var arrowOffsetParent = getOffsetParent(arrowElement);\n var clientSize = arrowOffsetParent ? axis === 'y' ? arrowOffsetParent.clientHeight || 0 : arrowOffsetParent.clientWidth || 0 : 0;\n var centerToReference = endDiff / 2 - startDiff / 2; // Make sure the arrow doesn't overflow the popper if the center point is\n // outside of the popper bounds\n\n var min = paddingObject[minProp];\n var max = clientSize - arrowRect[len] - paddingObject[maxProp];\n var center = clientSize / 2 - arrowRect[len] / 2 + centerToReference;\n var offset = within(min, center, max); // Prevents breaking syntax highlighting...\n\n var axisProp = axis;\n state.modifiersData[name] = (_state$modifiersData$ = {}, _state$modifiersData$[axisProp] = offset, _state$modifiersData$.centerOffset = offset - center, _state$modifiersData$);\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state,\n options = _ref2.options;\n var _options$element = options.element,\n arrowElement = _options$element === void 0 ? '[data-popper-arrow]' : _options$element;\n\n if (arrowElement == null) {\n return;\n } // CSS selector\n\n\n if (typeof arrowElement === 'string') {\n arrowElement = state.elements.popper.querySelector(arrowElement);\n\n if (!arrowElement) {\n return;\n }\n }\n\n if (!contains(state.elements.popper, arrowElement)) {\n return;\n }\n\n state.elements.arrow = arrowElement;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'arrow',\n enabled: true,\n phase: 'main',\n fn: arrow,\n effect: effect,\n requires: ['popperOffsets'],\n requiresIfExists: ['preventOverflow']\n};","export default function getVariation(placement) {\n return placement.split('-')[1];\n}","import { top, left, right, bottom, end } from \"../enums.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getWindow from \"../dom-utils/getWindow.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getComputedStyle from \"../dom-utils/getComputedStyle.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport { round } from \"../utils/math.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar unsetSides = {\n top: 'auto',\n right: 'auto',\n bottom: 'auto',\n left: 'auto'\n}; // Round the offsets to the nearest suitable subpixel based on the DPR.\n// Zooming can change the DPR, but it seems to report a value that will\n// cleanly divide the values into the appropriate subpixels.\n\nfunction roundOffsetsByDPR(_ref, win) {\n var x = _ref.x,\n y = _ref.y;\n var dpr = win.devicePixelRatio || 1;\n return {\n x: round(x * dpr) / dpr || 0,\n y: round(y * dpr) / dpr || 0\n };\n}\n\nexport function mapToStyles(_ref2) {\n var _Object$assign2;\n\n var popper = _ref2.popper,\n popperRect = _ref2.popperRect,\n placement = _ref2.placement,\n variation = _ref2.variation,\n offsets = _ref2.offsets,\n position = _ref2.position,\n gpuAcceleration = _ref2.gpuAcceleration,\n adaptive = _ref2.adaptive,\n roundOffsets = _ref2.roundOffsets,\n isFixed = _ref2.isFixed;\n var _offsets$x = offsets.x,\n x = _offsets$x === void 0 ? 0 : _offsets$x,\n _offsets$y = offsets.y,\n y = _offsets$y === void 0 ? 0 : _offsets$y;\n\n var _ref3 = typeof roundOffsets === 'function' ? roundOffsets({\n x: x,\n y: y\n }) : {\n x: x,\n y: y\n };\n\n x = _ref3.x;\n y = _ref3.y;\n var hasX = offsets.hasOwnProperty('x');\n var hasY = offsets.hasOwnProperty('y');\n var sideX = left;\n var sideY = top;\n var win = window;\n\n if (adaptive) {\n var offsetParent = getOffsetParent(popper);\n var heightProp = 'clientHeight';\n var widthProp = 'clientWidth';\n\n if (offsetParent === getWindow(popper)) {\n offsetParent = getDocumentElement(popper);\n\n if (getComputedStyle(offsetParent).position !== 'static' && position === 'absolute') {\n heightProp = 'scrollHeight';\n widthProp = 'scrollWidth';\n }\n } // $FlowFixMe[incompatible-cast]: force type refinement, we compare offsetParent with window above, but Flow doesn't detect it\n\n\n offsetParent = offsetParent;\n\n if (placement === top || (placement === left || placement === right) && variation === end) {\n sideY = bottom;\n var offsetY = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.height : // $FlowFixMe[prop-missing]\n offsetParent[heightProp];\n y -= offsetY - popperRect.height;\n y *= gpuAcceleration ? 1 : -1;\n }\n\n if (placement === left || (placement === top || placement === bottom) && variation === end) {\n sideX = right;\n var offsetX = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.width : // $FlowFixMe[prop-missing]\n offsetParent[widthProp];\n x -= offsetX - popperRect.width;\n x *= gpuAcceleration ? 1 : -1;\n }\n }\n\n var commonStyles = Object.assign({\n position: position\n }, adaptive && unsetSides);\n\n var _ref4 = roundOffsets === true ? roundOffsetsByDPR({\n x: x,\n y: y\n }, getWindow(popper)) : {\n x: x,\n y: y\n };\n\n x = _ref4.x;\n y = _ref4.y;\n\n if (gpuAcceleration) {\n var _Object$assign;\n\n return Object.assign({}, commonStyles, (_Object$assign = {}, _Object$assign[sideY] = hasY ? '0' : '', _Object$assign[sideX] = hasX ? '0' : '', _Object$assign.transform = (win.devicePixelRatio || 1) <= 1 ? \"translate(\" + x + \"px, \" + y + \"px)\" : \"translate3d(\" + x + \"px, \" + y + \"px, 0)\", _Object$assign));\n }\n\n return Object.assign({}, commonStyles, (_Object$assign2 = {}, _Object$assign2[sideY] = hasY ? y + \"px\" : '', _Object$assign2[sideX] = hasX ? x + \"px\" : '', _Object$assign2.transform = '', _Object$assign2));\n}\n\nfunction computeStyles(_ref5) {\n var state = _ref5.state,\n options = _ref5.options;\n var _options$gpuAccelerat = options.gpuAcceleration,\n gpuAcceleration = _options$gpuAccelerat === void 0 ? true : _options$gpuAccelerat,\n _options$adaptive = options.adaptive,\n adaptive = _options$adaptive === void 0 ? true : _options$adaptive,\n _options$roundOffsets = options.roundOffsets,\n roundOffsets = _options$roundOffsets === void 0 ? true : _options$roundOffsets;\n var commonStyles = {\n placement: getBasePlacement(state.placement),\n variation: getVariation(state.placement),\n popper: state.elements.popper,\n popperRect: state.rects.popper,\n gpuAcceleration: gpuAcceleration,\n isFixed: state.options.strategy === 'fixed'\n };\n\n if (state.modifiersData.popperOffsets != null) {\n state.styles.popper = Object.assign({}, state.styles.popper, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.popperOffsets,\n position: state.options.strategy,\n adaptive: adaptive,\n roundOffsets: roundOffsets\n })));\n }\n\n if (state.modifiersData.arrow != null) {\n state.styles.arrow = Object.assign({}, state.styles.arrow, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.arrow,\n position: 'absolute',\n adaptive: false,\n roundOffsets: roundOffsets\n })));\n }\n\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-placement': state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'computeStyles',\n enabled: true,\n phase: 'beforeWrite',\n fn: computeStyles,\n data: {}\n};","import getWindow from \"../dom-utils/getWindow.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar passive = {\n passive: true\n};\n\nfunction effect(_ref) {\n var state = _ref.state,\n instance = _ref.instance,\n options = _ref.options;\n var _options$scroll = options.scroll,\n scroll = _options$scroll === void 0 ? true : _options$scroll,\n _options$resize = options.resize,\n resize = _options$resize === void 0 ? true : _options$resize;\n var window = getWindow(state.elements.popper);\n var scrollParents = [].concat(state.scrollParents.reference, state.scrollParents.popper);\n\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.addEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.addEventListener('resize', instance.update, passive);\n }\n\n return function () {\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.removeEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.removeEventListener('resize', instance.update, passive);\n }\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'eventListeners',\n enabled: true,\n phase: 'write',\n fn: function fn() {},\n effect: effect,\n data: {}\n};","var hash = {\n left: 'right',\n right: 'left',\n bottom: 'top',\n top: 'bottom'\n};\nexport default function getOppositePlacement(placement) {\n return placement.replace(/left|right|bottom|top/g, function (matched) {\n return hash[matched];\n });\n}","var hash = {\n start: 'end',\n end: 'start'\n};\nexport default function getOppositeVariationPlacement(placement) {\n return placement.replace(/start|end/g, function (matched) {\n return hash[matched];\n });\n}","import getWindow from \"./getWindow.js\";\nexport default function getWindowScroll(node) {\n var win = getWindow(node);\n var scrollLeft = win.pageXOffset;\n var scrollTop = win.pageYOffset;\n return {\n scrollLeft: scrollLeft,\n scrollTop: scrollTop\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nexport default function getWindowScrollBarX(element) {\n // If has a CSS width greater than the viewport, then this will be\n // incorrect for RTL.\n // Popper 1 is broken in this case and never had a bug report so let's assume\n // it's not an issue. I don't think anyone ever specifies width on \n // anyway.\n // Browsers where the left scrollbar doesn't cause an issue report `0` for\n // this (e.g. Edge 2019, IE11, Safari)\n return getBoundingClientRect(getDocumentElement(element)).left + getWindowScroll(element).scrollLeft;\n}","import getComputedStyle from \"./getComputedStyle.js\";\nexport default function isScrollParent(element) {\n // Firefox wants us to check `-x` and `-y` variations as well\n var _getComputedStyle = getComputedStyle(element),\n overflow = _getComputedStyle.overflow,\n overflowX = _getComputedStyle.overflowX,\n overflowY = _getComputedStyle.overflowY;\n\n return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX);\n}","import getParentNode from \"./getParentNode.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nexport default function getScrollParent(node) {\n if (['html', 'body', '#document'].indexOf(getNodeName(node)) >= 0) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return node.ownerDocument.body;\n }\n\n if (isHTMLElement(node) && isScrollParent(node)) {\n return node;\n }\n\n return getScrollParent(getParentNode(node));\n}","import getScrollParent from \"./getScrollParent.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getWindow from \"./getWindow.js\";\nimport isScrollParent from \"./isScrollParent.js\";\n/*\ngiven a DOM element, return the list of all scroll parents, up the list of ancesors\nuntil we get to the top window object. This list is what we attach scroll listeners\nto, because if any of these parent elements scroll, we'll need to re-calculate the\nreference element's position.\n*/\n\nexport default function listScrollParents(element, list) {\n var _element$ownerDocumen;\n\n if (list === void 0) {\n list = [];\n }\n\n var scrollParent = getScrollParent(element);\n var isBody = scrollParent === ((_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body);\n var win = getWindow(scrollParent);\n var target = isBody ? [win].concat(win.visualViewport || [], isScrollParent(scrollParent) ? scrollParent : []) : scrollParent;\n var updatedList = list.concat(target);\n return isBody ? updatedList : // $FlowFixMe[incompatible-call]: isBody tells us target will be an HTMLElement here\n updatedList.concat(listScrollParents(getParentNode(target)));\n}","export default function rectToClientRect(rect) {\n return Object.assign({}, rect, {\n left: rect.x,\n top: rect.y,\n right: rect.x + rect.width,\n bottom: rect.y + rect.height\n });\n}","import { viewport } from \"../enums.js\";\nimport getViewportRect from \"./getViewportRect.js\";\nimport getDocumentRect from \"./getDocumentRect.js\";\nimport listScrollParents from \"./listScrollParents.js\";\nimport getOffsetParent from \"./getOffsetParent.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport contains from \"./contains.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport rectToClientRect from \"../utils/rectToClientRect.js\";\nimport { max, min } from \"../utils/math.js\";\n\nfunction getInnerBoundingClientRect(element, strategy) {\n var rect = getBoundingClientRect(element, false, strategy === 'fixed');\n rect.top = rect.top + element.clientTop;\n rect.left = rect.left + element.clientLeft;\n rect.bottom = rect.top + element.clientHeight;\n rect.right = rect.left + element.clientWidth;\n rect.width = element.clientWidth;\n rect.height = element.clientHeight;\n rect.x = rect.left;\n rect.y = rect.top;\n return rect;\n}\n\nfunction getClientRectFromMixedType(element, clippingParent, strategy) {\n return clippingParent === viewport ? rectToClientRect(getViewportRect(element, strategy)) : isElement(clippingParent) ? getInnerBoundingClientRect(clippingParent, strategy) : rectToClientRect(getDocumentRect(getDocumentElement(element)));\n} // A \"clipping parent\" is an overflowable container with the characteristic of\n// clipping (or hiding) overflowing elements with a position different from\n// `initial`\n\n\nfunction getClippingParents(element) {\n var clippingParents = listScrollParents(getParentNode(element));\n var canEscapeClipping = ['absolute', 'fixed'].indexOf(getComputedStyle(element).position) >= 0;\n var clipperElement = canEscapeClipping && isHTMLElement(element) ? getOffsetParent(element) : element;\n\n if (!isElement(clipperElement)) {\n return [];\n } // $FlowFixMe[incompatible-return]: https://github.com/facebook/flow/issues/1414\n\n\n return clippingParents.filter(function (clippingParent) {\n return isElement(clippingParent) && contains(clippingParent, clipperElement) && getNodeName(clippingParent) !== 'body';\n });\n} // Gets the maximum area that the element is visible in due to any number of\n// clipping parents\n\n\nexport default function getClippingRect(element, boundary, rootBoundary, strategy) {\n var mainClippingParents = boundary === 'clippingParents' ? getClippingParents(element) : [].concat(boundary);\n var clippingParents = [].concat(mainClippingParents, [rootBoundary]);\n var firstClippingParent = clippingParents[0];\n var clippingRect = clippingParents.reduce(function (accRect, clippingParent) {\n var rect = getClientRectFromMixedType(element, clippingParent, strategy);\n accRect.top = max(rect.top, accRect.top);\n accRect.right = min(rect.right, accRect.right);\n accRect.bottom = min(rect.bottom, accRect.bottom);\n accRect.left = max(rect.left, accRect.left);\n return accRect;\n }, getClientRectFromMixedType(element, firstClippingParent, strategy));\n clippingRect.width = clippingRect.right - clippingRect.left;\n clippingRect.height = clippingRect.bottom - clippingRect.top;\n clippingRect.x = clippingRect.left;\n clippingRect.y = clippingRect.top;\n return clippingRect;\n}","import getWindow from \"./getWindow.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport isLayoutViewport from \"./isLayoutViewport.js\";\nexport default function getViewportRect(element, strategy) {\n var win = getWindow(element);\n var html = getDocumentElement(element);\n var visualViewport = win.visualViewport;\n var width = html.clientWidth;\n var height = html.clientHeight;\n var x = 0;\n var y = 0;\n\n if (visualViewport) {\n width = visualViewport.width;\n height = visualViewport.height;\n var layoutViewport = isLayoutViewport();\n\n if (layoutViewport || !layoutViewport && strategy === 'fixed') {\n x = visualViewport.offsetLeft;\n y = visualViewport.offsetTop;\n }\n }\n\n return {\n width: width,\n height: height,\n x: x + getWindowScrollBarX(element),\n y: y\n };\n}","import getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nimport { max } from \"../utils/math.js\"; // Gets the entire size of the scrollable document area, even extending outside\n// of the `` and `` rect bounds if horizontally scrollable\n\nexport default function getDocumentRect(element) {\n var _element$ownerDocumen;\n\n var html = getDocumentElement(element);\n var winScroll = getWindowScroll(element);\n var body = (_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body;\n var width = max(html.scrollWidth, html.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0);\n var height = max(html.scrollHeight, html.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0);\n var x = -winScroll.scrollLeft + getWindowScrollBarX(element);\n var y = -winScroll.scrollTop;\n\n if (getComputedStyle(body || html).direction === 'rtl') {\n x += max(html.clientWidth, body ? body.clientWidth : 0) - width;\n }\n\n return {\n width: width,\n height: height,\n x: x,\n y: y\n };\n}","import getBasePlacement from \"./getBasePlacement.js\";\nimport getVariation from \"./getVariation.js\";\nimport getMainAxisFromPlacement from \"./getMainAxisFromPlacement.js\";\nimport { top, right, bottom, left, start, end } from \"../enums.js\";\nexport default function computeOffsets(_ref) {\n var reference = _ref.reference,\n element = _ref.element,\n placement = _ref.placement;\n var basePlacement = placement ? getBasePlacement(placement) : null;\n var variation = placement ? getVariation(placement) : null;\n var commonX = reference.x + reference.width / 2 - element.width / 2;\n var commonY = reference.y + reference.height / 2 - element.height / 2;\n var offsets;\n\n switch (basePlacement) {\n case top:\n offsets = {\n x: commonX,\n y: reference.y - element.height\n };\n break;\n\n case bottom:\n offsets = {\n x: commonX,\n y: reference.y + reference.height\n };\n break;\n\n case right:\n offsets = {\n x: reference.x + reference.width,\n y: commonY\n };\n break;\n\n case left:\n offsets = {\n x: reference.x - element.width,\n y: commonY\n };\n break;\n\n default:\n offsets = {\n x: reference.x,\n y: reference.y\n };\n }\n\n var mainAxis = basePlacement ? getMainAxisFromPlacement(basePlacement) : null;\n\n if (mainAxis != null) {\n var len = mainAxis === 'y' ? 'height' : 'width';\n\n switch (variation) {\n case start:\n offsets[mainAxis] = offsets[mainAxis] - (reference[len] / 2 - element[len] / 2);\n break;\n\n case end:\n offsets[mainAxis] = offsets[mainAxis] + (reference[len] / 2 - element[len] / 2);\n break;\n\n default:\n }\n }\n\n return offsets;\n}","import getClippingRect from \"../dom-utils/getClippingRect.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getBoundingClientRect from \"../dom-utils/getBoundingClientRect.js\";\nimport computeOffsets from \"./computeOffsets.js\";\nimport rectToClientRect from \"./rectToClientRect.js\";\nimport { clippingParents, reference, popper, bottom, top, right, basePlacements, viewport } from \"../enums.js\";\nimport { isElement } from \"../dom-utils/instanceOf.js\";\nimport mergePaddingObject from \"./mergePaddingObject.js\";\nimport expandToHashMap from \"./expandToHashMap.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport default function detectOverflow(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n _options$placement = _options.placement,\n placement = _options$placement === void 0 ? state.placement : _options$placement,\n _options$strategy = _options.strategy,\n strategy = _options$strategy === void 0 ? state.strategy : _options$strategy,\n _options$boundary = _options.boundary,\n boundary = _options$boundary === void 0 ? clippingParents : _options$boundary,\n _options$rootBoundary = _options.rootBoundary,\n rootBoundary = _options$rootBoundary === void 0 ? viewport : _options$rootBoundary,\n _options$elementConte = _options.elementContext,\n elementContext = _options$elementConte === void 0 ? popper : _options$elementConte,\n _options$altBoundary = _options.altBoundary,\n altBoundary = _options$altBoundary === void 0 ? false : _options$altBoundary,\n _options$padding = _options.padding,\n padding = _options$padding === void 0 ? 0 : _options$padding;\n var paddingObject = mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n var altContext = elementContext === popper ? reference : popper;\n var popperRect = state.rects.popper;\n var element = state.elements[altBoundary ? altContext : elementContext];\n var clippingClientRect = getClippingRect(isElement(element) ? element : element.contextElement || getDocumentElement(state.elements.popper), boundary, rootBoundary, strategy);\n var referenceClientRect = getBoundingClientRect(state.elements.reference);\n var popperOffsets = computeOffsets({\n reference: referenceClientRect,\n element: popperRect,\n strategy: 'absolute',\n placement: placement\n });\n var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets));\n var elementClientRect = elementContext === popper ? popperClientRect : referenceClientRect; // positive = overflowing the clipping rect\n // 0 or negative = within the clipping rect\n\n var overflowOffsets = {\n top: clippingClientRect.top - elementClientRect.top + paddingObject.top,\n bottom: elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom,\n left: clippingClientRect.left - elementClientRect.left + paddingObject.left,\n right: elementClientRect.right - clippingClientRect.right + paddingObject.right\n };\n var offsetData = state.modifiersData.offset; // Offsets can be applied only to the popper element\n\n if (elementContext === popper && offsetData) {\n var offset = offsetData[placement];\n Object.keys(overflowOffsets).forEach(function (key) {\n var multiply = [right, bottom].indexOf(key) >= 0 ? 1 : -1;\n var axis = [top, bottom].indexOf(key) >= 0 ? 'y' : 'x';\n overflowOffsets[key] += offset[axis] * multiply;\n });\n }\n\n return overflowOffsets;\n}","import getOppositePlacement from \"../utils/getOppositePlacement.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getOppositeVariationPlacement from \"../utils/getOppositeVariationPlacement.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport computeAutoPlacement from \"../utils/computeAutoPlacement.js\";\nimport { bottom, top, start, right, left, auto } from \"../enums.js\";\nimport getVariation from \"../utils/getVariation.js\"; // eslint-disable-next-line import/no-unused-modules\n\nfunction getExpandedFallbackPlacements(placement) {\n if (getBasePlacement(placement) === auto) {\n return [];\n }\n\n var oppositePlacement = getOppositePlacement(placement);\n return [getOppositeVariationPlacement(placement), oppositePlacement, getOppositeVariationPlacement(oppositePlacement)];\n}\n\nfunction flip(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n\n if (state.modifiersData[name]._skip) {\n return;\n }\n\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? true : _options$altAxis,\n specifiedFallbackPlacements = options.fallbackPlacements,\n padding = options.padding,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n _options$flipVariatio = options.flipVariations,\n flipVariations = _options$flipVariatio === void 0 ? true : _options$flipVariatio,\n allowedAutoPlacements = options.allowedAutoPlacements;\n var preferredPlacement = state.options.placement;\n var basePlacement = getBasePlacement(preferredPlacement);\n var isBasePlacement = basePlacement === preferredPlacement;\n var fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipVariations ? [getOppositePlacement(preferredPlacement)] : getExpandedFallbackPlacements(preferredPlacement));\n var placements = [preferredPlacement].concat(fallbackPlacements).reduce(function (acc, placement) {\n return acc.concat(getBasePlacement(placement) === auto ? computeAutoPlacement(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n flipVariations: flipVariations,\n allowedAutoPlacements: allowedAutoPlacements\n }) : placement);\n }, []);\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var checksMap = new Map();\n var makeFallbackChecks = true;\n var firstFittingPlacement = placements[0];\n\n for (var i = 0; i < placements.length; i++) {\n var placement = placements[i];\n\n var _basePlacement = getBasePlacement(placement);\n\n var isStartVariation = getVariation(placement) === start;\n var isVertical = [top, bottom].indexOf(_basePlacement) >= 0;\n var len = isVertical ? 'width' : 'height';\n var overflow = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n altBoundary: altBoundary,\n padding: padding\n });\n var mainVariationSide = isVertical ? isStartVariation ? right : left : isStartVariation ? bottom : top;\n\n if (referenceRect[len] > popperRect[len]) {\n mainVariationSide = getOppositePlacement(mainVariationSide);\n }\n\n var altVariationSide = getOppositePlacement(mainVariationSide);\n var checks = [];\n\n if (checkMainAxis) {\n checks.push(overflow[_basePlacement] <= 0);\n }\n\n if (checkAltAxis) {\n checks.push(overflow[mainVariationSide] <= 0, overflow[altVariationSide] <= 0);\n }\n\n if (checks.every(function (check) {\n return check;\n })) {\n firstFittingPlacement = placement;\n makeFallbackChecks = false;\n break;\n }\n\n checksMap.set(placement, checks);\n }\n\n if (makeFallbackChecks) {\n // `2` may be desired in some cases – research later\n var numberOfChecks = flipVariations ? 3 : 1;\n\n var _loop = function _loop(_i) {\n var fittingPlacement = placements.find(function (placement) {\n var checks = checksMap.get(placement);\n\n if (checks) {\n return checks.slice(0, _i).every(function (check) {\n return check;\n });\n }\n });\n\n if (fittingPlacement) {\n firstFittingPlacement = fittingPlacement;\n return \"break\";\n }\n };\n\n for (var _i = numberOfChecks; _i > 0; _i--) {\n var _ret = _loop(_i);\n\n if (_ret === \"break\") break;\n }\n }\n\n if (state.placement !== firstFittingPlacement) {\n state.modifiersData[name]._skip = true;\n state.placement = firstFittingPlacement;\n state.reset = true;\n }\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'flip',\n enabled: true,\n phase: 'main',\n fn: flip,\n requiresIfExists: ['offset'],\n data: {\n _skip: false\n }\n};","import getVariation from \"./getVariation.js\";\nimport { variationPlacements, basePlacements, placements as allPlacements } from \"../enums.js\";\nimport detectOverflow from \"./detectOverflow.js\";\nimport getBasePlacement from \"./getBasePlacement.js\";\nexport default function computeAutoPlacement(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n placement = _options.placement,\n boundary = _options.boundary,\n rootBoundary = _options.rootBoundary,\n padding = _options.padding,\n flipVariations = _options.flipVariations,\n _options$allowedAutoP = _options.allowedAutoPlacements,\n allowedAutoPlacements = _options$allowedAutoP === void 0 ? allPlacements : _options$allowedAutoP;\n var variation = getVariation(placement);\n var placements = variation ? flipVariations ? variationPlacements : variationPlacements.filter(function (placement) {\n return getVariation(placement) === variation;\n }) : basePlacements;\n var allowedPlacements = placements.filter(function (placement) {\n return allowedAutoPlacements.indexOf(placement) >= 0;\n });\n\n if (allowedPlacements.length === 0) {\n allowedPlacements = placements;\n } // $FlowFixMe[incompatible-type]: Flow seems to have problems with two array unions...\n\n\n var overflows = allowedPlacements.reduce(function (acc, placement) {\n acc[placement] = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding\n })[getBasePlacement(placement)];\n return acc;\n }, {});\n return Object.keys(overflows).sort(function (a, b) {\n return overflows[a] - overflows[b];\n });\n}","import { top, bottom, left, right } from \"../enums.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\n\nfunction getSideOffsets(overflow, rect, preventedOffsets) {\n if (preventedOffsets === void 0) {\n preventedOffsets = {\n x: 0,\n y: 0\n };\n }\n\n return {\n top: overflow.top - rect.height - preventedOffsets.y,\n right: overflow.right - rect.width + preventedOffsets.x,\n bottom: overflow.bottom - rect.height + preventedOffsets.y,\n left: overflow.left - rect.width - preventedOffsets.x\n };\n}\n\nfunction isAnySideFullyClipped(overflow) {\n return [top, right, bottom, left].some(function (side) {\n return overflow[side] >= 0;\n });\n}\n\nfunction hide(_ref) {\n var state = _ref.state,\n name = _ref.name;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var preventedOffsets = state.modifiersData.preventOverflow;\n var referenceOverflow = detectOverflow(state, {\n elementContext: 'reference'\n });\n var popperAltOverflow = detectOverflow(state, {\n altBoundary: true\n });\n var referenceClippingOffsets = getSideOffsets(referenceOverflow, referenceRect);\n var popperEscapeOffsets = getSideOffsets(popperAltOverflow, popperRect, preventedOffsets);\n var isReferenceHidden = isAnySideFullyClipped(referenceClippingOffsets);\n var hasPopperEscaped = isAnySideFullyClipped(popperEscapeOffsets);\n state.modifiersData[name] = {\n referenceClippingOffsets: referenceClippingOffsets,\n popperEscapeOffsets: popperEscapeOffsets,\n isReferenceHidden: isReferenceHidden,\n hasPopperEscaped: hasPopperEscaped\n };\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-reference-hidden': isReferenceHidden,\n 'data-popper-escaped': hasPopperEscaped\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'hide',\n enabled: true,\n phase: 'main',\n requiresIfExists: ['preventOverflow'],\n fn: hide\n};","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport { top, left, right, placements } from \"../enums.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport function distanceAndSkiddingToXY(placement, rects, offset) {\n var basePlacement = getBasePlacement(placement);\n var invertDistance = [left, top].indexOf(basePlacement) >= 0 ? -1 : 1;\n\n var _ref = typeof offset === 'function' ? offset(Object.assign({}, rects, {\n placement: placement\n })) : offset,\n skidding = _ref[0],\n distance = _ref[1];\n\n skidding = skidding || 0;\n distance = (distance || 0) * invertDistance;\n return [left, right].indexOf(basePlacement) >= 0 ? {\n x: distance,\n y: skidding\n } : {\n x: skidding,\n y: distance\n };\n}\n\nfunction offset(_ref2) {\n var state = _ref2.state,\n options = _ref2.options,\n name = _ref2.name;\n var _options$offset = options.offset,\n offset = _options$offset === void 0 ? [0, 0] : _options$offset;\n var data = placements.reduce(function (acc, placement) {\n acc[placement] = distanceAndSkiddingToXY(placement, state.rects, offset);\n return acc;\n }, {});\n var _data$state$placement = data[state.placement],\n x = _data$state$placement.x,\n y = _data$state$placement.y;\n\n if (state.modifiersData.popperOffsets != null) {\n state.modifiersData.popperOffsets.x += x;\n state.modifiersData.popperOffsets.y += y;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'offset',\n enabled: true,\n phase: 'main',\n requires: ['popperOffsets'],\n fn: offset\n};","import computeOffsets from \"../utils/computeOffsets.js\";\n\nfunction popperOffsets(_ref) {\n var state = _ref.state,\n name = _ref.name;\n // Offsets are the actual position the popper needs to have to be\n // properly positioned near its reference element\n // This is the most basic placement, and will be adjusted by\n // the modifiers in the next step\n state.modifiersData[name] = computeOffsets({\n reference: state.rects.reference,\n element: state.rects.popper,\n strategy: 'absolute',\n placement: state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'popperOffsets',\n enabled: true,\n phase: 'read',\n fn: popperOffsets,\n data: {}\n};","import { top, left, right, bottom, start } from \"../enums.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport getAltAxis from \"../utils/getAltAxis.js\";\nimport { within, withinMaxClamp } from \"../utils/within.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport getFreshSideObject from \"../utils/getFreshSideObject.js\";\nimport { min as mathMin, max as mathMax } from \"../utils/math.js\";\n\nfunction preventOverflow(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? false : _options$altAxis,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n padding = options.padding,\n _options$tether = options.tether,\n tether = _options$tether === void 0 ? true : _options$tether,\n _options$tetherOffset = options.tetherOffset,\n tetherOffset = _options$tetherOffset === void 0 ? 0 : _options$tetherOffset;\n var overflow = detectOverflow(state, {\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n altBoundary: altBoundary\n });\n var basePlacement = getBasePlacement(state.placement);\n var variation = getVariation(state.placement);\n var isBasePlacement = !variation;\n var mainAxis = getMainAxisFromPlacement(basePlacement);\n var altAxis = getAltAxis(mainAxis);\n var popperOffsets = state.modifiersData.popperOffsets;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var tetherOffsetValue = typeof tetherOffset === 'function' ? tetherOffset(Object.assign({}, state.rects, {\n placement: state.placement\n })) : tetherOffset;\n var normalizedTetherOffsetValue = typeof tetherOffsetValue === 'number' ? {\n mainAxis: tetherOffsetValue,\n altAxis: tetherOffsetValue\n } : Object.assign({\n mainAxis: 0,\n altAxis: 0\n }, tetherOffsetValue);\n var offsetModifierState = state.modifiersData.offset ? state.modifiersData.offset[state.placement] : null;\n var data = {\n x: 0,\n y: 0\n };\n\n if (!popperOffsets) {\n return;\n }\n\n if (checkMainAxis) {\n var _offsetModifierState$;\n\n var mainSide = mainAxis === 'y' ? top : left;\n var altSide = mainAxis === 'y' ? bottom : right;\n var len = mainAxis === 'y' ? 'height' : 'width';\n var offset = popperOffsets[mainAxis];\n var min = offset + overflow[mainSide];\n var max = offset - overflow[altSide];\n var additive = tether ? -popperRect[len] / 2 : 0;\n var minLen = variation === start ? referenceRect[len] : popperRect[len];\n var maxLen = variation === start ? -popperRect[len] : -referenceRect[len]; // We need to include the arrow in the calculation so the arrow doesn't go\n // outside the reference bounds\n\n var arrowElement = state.elements.arrow;\n var arrowRect = tether && arrowElement ? getLayoutRect(arrowElement) : {\n width: 0,\n height: 0\n };\n var arrowPaddingObject = state.modifiersData['arrow#persistent'] ? state.modifiersData['arrow#persistent'].padding : getFreshSideObject();\n var arrowPaddingMin = arrowPaddingObject[mainSide];\n var arrowPaddingMax = arrowPaddingObject[altSide]; // If the reference length is smaller than the arrow length, we don't want\n // to include its full size in the calculation. If the reference is small\n // and near the edge of a boundary, the popper can overflow even if the\n // reference is not overflowing as well (e.g. virtual elements with no\n // width or height)\n\n var arrowLen = within(0, referenceRect[len], arrowRect[len]);\n var minOffset = isBasePlacement ? referenceRect[len] / 2 - additive - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis : minLen - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis;\n var maxOffset = isBasePlacement ? -referenceRect[len] / 2 + additive + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis : maxLen + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis;\n var arrowOffsetParent = state.elements.arrow && getOffsetParent(state.elements.arrow);\n var clientOffset = arrowOffsetParent ? mainAxis === 'y' ? arrowOffsetParent.clientTop || 0 : arrowOffsetParent.clientLeft || 0 : 0;\n var offsetModifierValue = (_offsetModifierState$ = offsetModifierState == null ? void 0 : offsetModifierState[mainAxis]) != null ? _offsetModifierState$ : 0;\n var tetherMin = offset + minOffset - offsetModifierValue - clientOffset;\n var tetherMax = offset + maxOffset - offsetModifierValue;\n var preventedOffset = within(tether ? mathMin(min, tetherMin) : min, offset, tether ? mathMax(max, tetherMax) : max);\n popperOffsets[mainAxis] = preventedOffset;\n data[mainAxis] = preventedOffset - offset;\n }\n\n if (checkAltAxis) {\n var _offsetModifierState$2;\n\n var _mainSide = mainAxis === 'x' ? top : left;\n\n var _altSide = mainAxis === 'x' ? bottom : right;\n\n var _offset = popperOffsets[altAxis];\n\n var _len = altAxis === 'y' ? 'height' : 'width';\n\n var _min = _offset + overflow[_mainSide];\n\n var _max = _offset - overflow[_altSide];\n\n var isOriginSide = [top, left].indexOf(basePlacement) !== -1;\n\n var _offsetModifierValue = (_offsetModifierState$2 = offsetModifierState == null ? void 0 : offsetModifierState[altAxis]) != null ? _offsetModifierState$2 : 0;\n\n var _tetherMin = isOriginSide ? _min : _offset - referenceRect[_len] - popperRect[_len] - _offsetModifierValue + normalizedTetherOffsetValue.altAxis;\n\n var _tetherMax = isOriginSide ? _offset + referenceRect[_len] + popperRect[_len] - _offsetModifierValue - normalizedTetherOffsetValue.altAxis : _max;\n\n var _preventedOffset = tether && isOriginSide ? withinMaxClamp(_tetherMin, _offset, _tetherMax) : within(tether ? _tetherMin : _min, _offset, tether ? _tetherMax : _max);\n\n popperOffsets[altAxis] = _preventedOffset;\n data[altAxis] = _preventedOffset - _offset;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'preventOverflow',\n enabled: true,\n phase: 'main',\n fn: preventOverflow,\n requiresIfExists: ['offset']\n};","export default function getAltAxis(axis) {\n return axis === 'x' ? 'y' : 'x';\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getNodeScroll from \"./getNodeScroll.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport { round } from \"../utils/math.js\";\n\nfunction isElementScaled(element) {\n var rect = element.getBoundingClientRect();\n var scaleX = round(rect.width) / element.offsetWidth || 1;\n var scaleY = round(rect.height) / element.offsetHeight || 1;\n return scaleX !== 1 || scaleY !== 1;\n} // Returns the composite rect of an element relative to its offsetParent.\n// Composite means it takes into account transforms as well as layout.\n\n\nexport default function getCompositeRect(elementOrVirtualElement, offsetParent, isFixed) {\n if (isFixed === void 0) {\n isFixed = false;\n }\n\n var isOffsetParentAnElement = isHTMLElement(offsetParent);\n var offsetParentIsScaled = isHTMLElement(offsetParent) && isElementScaled(offsetParent);\n var documentElement = getDocumentElement(offsetParent);\n var rect = getBoundingClientRect(elementOrVirtualElement, offsetParentIsScaled, isFixed);\n var scroll = {\n scrollLeft: 0,\n scrollTop: 0\n };\n var offsets = {\n x: 0,\n y: 0\n };\n\n if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {\n if (getNodeName(offsetParent) !== 'body' || // https://github.com/popperjs/popper-core/issues/1078\n isScrollParent(documentElement)) {\n scroll = getNodeScroll(offsetParent);\n }\n\n if (isHTMLElement(offsetParent)) {\n offsets = getBoundingClientRect(offsetParent, true);\n offsets.x += offsetParent.clientLeft;\n offsets.y += offsetParent.clientTop;\n } else if (documentElement) {\n offsets.x = getWindowScrollBarX(documentElement);\n }\n }\n\n return {\n x: rect.left + scroll.scrollLeft - offsets.x,\n y: rect.top + scroll.scrollTop - offsets.y,\n width: rect.width,\n height: rect.height\n };\n}","import getWindowScroll from \"./getWindowScroll.js\";\nimport getWindow from \"./getWindow.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getHTMLElementScroll from \"./getHTMLElementScroll.js\";\nexport default function getNodeScroll(node) {\n if (node === getWindow(node) || !isHTMLElement(node)) {\n return getWindowScroll(node);\n } else {\n return getHTMLElementScroll(node);\n }\n}","export default function getHTMLElementScroll(element) {\n return {\n scrollLeft: element.scrollLeft,\n scrollTop: element.scrollTop\n };\n}","import { modifierPhases } from \"../enums.js\"; // source: https://stackoverflow.com/questions/49875255\n\nfunction order(modifiers) {\n var map = new Map();\n var visited = new Set();\n var result = [];\n modifiers.forEach(function (modifier) {\n map.set(modifier.name, modifier);\n }); // On visiting object, check for its dependencies and visit them recursively\n\n function sort(modifier) {\n visited.add(modifier.name);\n var requires = [].concat(modifier.requires || [], modifier.requiresIfExists || []);\n requires.forEach(function (dep) {\n if (!visited.has(dep)) {\n var depModifier = map.get(dep);\n\n if (depModifier) {\n sort(depModifier);\n }\n }\n });\n result.push(modifier);\n }\n\n modifiers.forEach(function (modifier) {\n if (!visited.has(modifier.name)) {\n // check for visited object\n sort(modifier);\n }\n });\n return result;\n}\n\nexport default function orderModifiers(modifiers) {\n // order based on dependencies\n var orderedModifiers = order(modifiers); // order based on phase\n\n return modifierPhases.reduce(function (acc, phase) {\n return acc.concat(orderedModifiers.filter(function (modifier) {\n return modifier.phase === phase;\n }));\n }, []);\n}","import getCompositeRect from \"./dom-utils/getCompositeRect.js\";\nimport getLayoutRect from \"./dom-utils/getLayoutRect.js\";\nimport listScrollParents from \"./dom-utils/listScrollParents.js\";\nimport getOffsetParent from \"./dom-utils/getOffsetParent.js\";\nimport orderModifiers from \"./utils/orderModifiers.js\";\nimport debounce from \"./utils/debounce.js\";\nimport mergeByName from \"./utils/mergeByName.js\";\nimport detectOverflow from \"./utils/detectOverflow.js\";\nimport { isElement } from \"./dom-utils/instanceOf.js\";\nvar DEFAULT_OPTIONS = {\n placement: 'bottom',\n modifiers: [],\n strategy: 'absolute'\n};\n\nfunction areValidElements() {\n for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n return !args.some(function (element) {\n return !(element && typeof element.getBoundingClientRect === 'function');\n });\n}\n\nexport function popperGenerator(generatorOptions) {\n if (generatorOptions === void 0) {\n generatorOptions = {};\n }\n\n var _generatorOptions = generatorOptions,\n _generatorOptions$def = _generatorOptions.defaultModifiers,\n defaultModifiers = _generatorOptions$def === void 0 ? [] : _generatorOptions$def,\n _generatorOptions$def2 = _generatorOptions.defaultOptions,\n defaultOptions = _generatorOptions$def2 === void 0 ? DEFAULT_OPTIONS : _generatorOptions$def2;\n return function createPopper(reference, popper, options) {\n if (options === void 0) {\n options = defaultOptions;\n }\n\n var state = {\n placement: 'bottom',\n orderedModifiers: [],\n options: Object.assign({}, DEFAULT_OPTIONS, defaultOptions),\n modifiersData: {},\n elements: {\n reference: reference,\n popper: popper\n },\n attributes: {},\n styles: {}\n };\n var effectCleanupFns = [];\n var isDestroyed = false;\n var instance = {\n state: state,\n setOptions: function setOptions(setOptionsAction) {\n var options = typeof setOptionsAction === 'function' ? setOptionsAction(state.options) : setOptionsAction;\n cleanupModifierEffects();\n state.options = Object.assign({}, defaultOptions, state.options, options);\n state.scrollParents = {\n reference: isElement(reference) ? listScrollParents(reference) : reference.contextElement ? listScrollParents(reference.contextElement) : [],\n popper: listScrollParents(popper)\n }; // Orders the modifiers based on their dependencies and `phase`\n // properties\n\n var orderedModifiers = orderModifiers(mergeByName([].concat(defaultModifiers, state.options.modifiers))); // Strip out disabled modifiers\n\n state.orderedModifiers = orderedModifiers.filter(function (m) {\n return m.enabled;\n });\n runModifierEffects();\n return instance.update();\n },\n // Sync update – it will always be executed, even if not necessary. This\n // is useful for low frequency updates where sync behavior simplifies the\n // logic.\n // For high frequency updates (e.g. `resize` and `scroll` events), always\n // prefer the async Popper#update method\n forceUpdate: function forceUpdate() {\n if (isDestroyed) {\n return;\n }\n\n var _state$elements = state.elements,\n reference = _state$elements.reference,\n popper = _state$elements.popper; // Don't proceed if `reference` or `popper` are not valid elements\n // anymore\n\n if (!areValidElements(reference, popper)) {\n return;\n } // Store the reference and popper rects to be read by modifiers\n\n\n state.rects = {\n reference: getCompositeRect(reference, getOffsetParent(popper), state.options.strategy === 'fixed'),\n popper: getLayoutRect(popper)\n }; // Modifiers have the ability to reset the current update cycle. The\n // most common use case for this is the `flip` modifier changing the\n // placement, which then needs to re-run all the modifiers, because the\n // logic was previously ran for the previous placement and is therefore\n // stale/incorrect\n\n state.reset = false;\n state.placement = state.options.placement; // On each update cycle, the `modifiersData` property for each modifier\n // is filled with the initial data specified by the modifier. This means\n // it doesn't persist and is fresh on each update.\n // To ensure persistent data, use `${name}#persistent`\n\n state.orderedModifiers.forEach(function (modifier) {\n return state.modifiersData[modifier.name] = Object.assign({}, modifier.data);\n });\n\n for (var index = 0; index < state.orderedModifiers.length; index++) {\n if (state.reset === true) {\n state.reset = false;\n index = -1;\n continue;\n }\n\n var _state$orderedModifie = state.orderedModifiers[index],\n fn = _state$orderedModifie.fn,\n _state$orderedModifie2 = _state$orderedModifie.options,\n _options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2,\n name = _state$orderedModifie.name;\n\n if (typeof fn === 'function') {\n state = fn({\n state: state,\n options: _options,\n name: name,\n instance: instance\n }) || state;\n }\n }\n },\n // Async and optimistically optimized update – it will not be executed if\n // not necessary (debounced to run at most once-per-tick)\n update: debounce(function () {\n return new Promise(function (resolve) {\n instance.forceUpdate();\n resolve(state);\n });\n }),\n destroy: function destroy() {\n cleanupModifierEffects();\n isDestroyed = true;\n }\n };\n\n if (!areValidElements(reference, popper)) {\n return instance;\n }\n\n instance.setOptions(options).then(function (state) {\n if (!isDestroyed && options.onFirstUpdate) {\n options.onFirstUpdate(state);\n }\n }); // Modifiers have the ability to execute arbitrary code before the first\n // update cycle runs. They will be executed in the same order as the update\n // cycle. This is useful when a modifier adds some persistent data that\n // other modifiers need to use, but the modifier is run after the dependent\n // one.\n\n function runModifierEffects() {\n state.orderedModifiers.forEach(function (_ref) {\n var name = _ref.name,\n _ref$options = _ref.options,\n options = _ref$options === void 0 ? {} : _ref$options,\n effect = _ref.effect;\n\n if (typeof effect === 'function') {\n var cleanupFn = effect({\n state: state,\n name: name,\n instance: instance,\n options: options\n });\n\n var noopFn = function noopFn() {};\n\n effectCleanupFns.push(cleanupFn || noopFn);\n }\n });\n }\n\n function cleanupModifierEffects() {\n effectCleanupFns.forEach(function (fn) {\n return fn();\n });\n effectCleanupFns = [];\n }\n\n return instance;\n };\n}\nexport var createPopper = /*#__PURE__*/popperGenerator(); // eslint-disable-next-line import/no-unused-modules\n\nexport { detectOverflow };","export default function debounce(fn) {\n var pending;\n return function () {\n if (!pending) {\n pending = new Promise(function (resolve) {\n Promise.resolve().then(function () {\n pending = undefined;\n resolve(fn());\n });\n });\n }\n\n return pending;\n };\n}","export default function mergeByName(modifiers) {\n var merged = modifiers.reduce(function (merged, current) {\n var existing = merged[current.name];\n merged[current.name] = existing ? Object.assign({}, existing, current, {\n options: Object.assign({}, existing.options, current.options),\n data: Object.assign({}, existing.data, current.data)\n }) : current;\n return merged;\n }, {}); // IE11 does not support Object.values\n\n return Object.keys(merged).map(function (key) {\n return merged[key];\n });\n}","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nimport offset from \"./modifiers/offset.js\";\nimport flip from \"./modifiers/flip.js\";\nimport preventOverflow from \"./modifiers/preventOverflow.js\";\nimport arrow from \"./modifiers/arrow.js\";\nimport hide from \"./modifiers/hide.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles, offset, flip, preventOverflow, arrow, hide];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow }; // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper as createPopperLite } from \"./popper-lite.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport * from \"./modifiers/index.js\";","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow };","/*!\n * Bootstrap v5.3.2 (https://getbootstrap.com/)\n * Copyright 2011-2023 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\nimport * as Popper from '@popperjs/core';\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/data.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n/**\n * Constants\n */\n\nconst elementMap = new Map();\nconst Data = {\n set(element, key, instance) {\n if (!elementMap.has(element)) {\n elementMap.set(element, new Map());\n }\n const instanceMap = elementMap.get(element);\n\n // make it clear we only want one instance per element\n // can be removed later when multiple key/instances are fine to be used\n if (!instanceMap.has(key) && instanceMap.size !== 0) {\n // eslint-disable-next-line no-console\n console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`);\n return;\n }\n instanceMap.set(key, instance);\n },\n get(element, key) {\n if (elementMap.has(element)) {\n return elementMap.get(element).get(key) || null;\n }\n return null;\n },\n remove(element, key) {\n if (!elementMap.has(element)) {\n return;\n }\n const instanceMap = elementMap.get(element);\n instanceMap.delete(key);\n\n // free up element references if there are no instances left for an element\n if (instanceMap.size === 0) {\n elementMap.delete(element);\n }\n }\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/index.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst MAX_UID = 1000000;\nconst MILLISECONDS_MULTIPLIER = 1000;\nconst TRANSITION_END = 'transitionend';\n\n/**\n * Properly escape IDs selectors to handle weird IDs\n * @param {string} selector\n * @returns {string}\n */\nconst parseSelector = selector => {\n if (selector && window.CSS && window.CSS.escape) {\n // document.querySelector needs escaping to handle IDs (html5+) containing for instance /\n selector = selector.replace(/#([^\\s\"#']+)/g, (match, id) => `#${CSS.escape(id)}`);\n }\n return selector;\n};\n\n// Shout-out Angus Croll (https://goo.gl/pxwQGp)\nconst toType = object => {\n if (object === null || object === undefined) {\n return `${object}`;\n }\n return Object.prototype.toString.call(object).match(/\\s([a-z]+)/i)[1].toLowerCase();\n};\n\n/**\n * Public Util API\n */\n\nconst getUID = prefix => {\n do {\n prefix += Math.floor(Math.random() * MAX_UID);\n } while (document.getElementById(prefix));\n return prefix;\n};\nconst getTransitionDurationFromElement = element => {\n if (!element) {\n return 0;\n }\n\n // Get transition-duration of the element\n let {\n transitionDuration,\n transitionDelay\n } = window.getComputedStyle(element);\n const floatTransitionDuration = Number.parseFloat(transitionDuration);\n const floatTransitionDelay = Number.parseFloat(transitionDelay);\n\n // Return 0 if element or transition duration is not found\n if (!floatTransitionDuration && !floatTransitionDelay) {\n return 0;\n }\n\n // If multiple durations are defined, take the first\n transitionDuration = transitionDuration.split(',')[0];\n transitionDelay = transitionDelay.split(',')[0];\n return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER;\n};\nconst triggerTransitionEnd = element => {\n element.dispatchEvent(new Event(TRANSITION_END));\n};\nconst isElement = object => {\n if (!object || typeof object !== 'object') {\n return false;\n }\n if (typeof object.jquery !== 'undefined') {\n object = object[0];\n }\n return typeof object.nodeType !== 'undefined';\n};\nconst getElement = object => {\n // it's a jQuery object or a node element\n if (isElement(object)) {\n return object.jquery ? object[0] : object;\n }\n if (typeof object === 'string' && object.length > 0) {\n return document.querySelector(parseSelector(object));\n }\n return null;\n};\nconst isVisible = element => {\n if (!isElement(element) || element.getClientRects().length === 0) {\n return false;\n }\n const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible';\n // Handle `details` element as its content may falsie appear visible when it is closed\n const closedDetails = element.closest('details:not([open])');\n if (!closedDetails) {\n return elementIsVisible;\n }\n if (closedDetails !== element) {\n const summary = element.closest('summary');\n if (summary && summary.parentNode !== closedDetails) {\n return false;\n }\n if (summary === null) {\n return false;\n }\n }\n return elementIsVisible;\n};\nconst isDisabled = element => {\n if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n return true;\n }\n if (element.classList.contains('disabled')) {\n return true;\n }\n if (typeof element.disabled !== 'undefined') {\n return element.disabled;\n }\n return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false';\n};\nconst findShadowRoot = element => {\n if (!document.documentElement.attachShadow) {\n return null;\n }\n\n // Can find the shadow root otherwise it'll return the document\n if (typeof element.getRootNode === 'function') {\n const root = element.getRootNode();\n return root instanceof ShadowRoot ? root : null;\n }\n if (element instanceof ShadowRoot) {\n return element;\n }\n\n // when we don't find a shadow root\n if (!element.parentNode) {\n return null;\n }\n return findShadowRoot(element.parentNode);\n};\nconst noop = () => {};\n\n/**\n * Trick to restart an element's animation\n *\n * @param {HTMLElement} element\n * @return void\n *\n * @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation\n */\nconst reflow = element => {\n element.offsetHeight; // eslint-disable-line no-unused-expressions\n};\n\nconst getjQuery = () => {\n if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {\n return window.jQuery;\n }\n return null;\n};\nconst DOMContentLoadedCallbacks = [];\nconst onDOMContentLoaded = callback => {\n if (document.readyState === 'loading') {\n // add listener on the first call when the document is in loading state\n if (!DOMContentLoadedCallbacks.length) {\n document.addEventListener('DOMContentLoaded', () => {\n for (const callback of DOMContentLoadedCallbacks) {\n callback();\n }\n });\n }\n DOMContentLoadedCallbacks.push(callback);\n } else {\n callback();\n }\n};\nconst isRTL = () => document.documentElement.dir === 'rtl';\nconst defineJQueryPlugin = plugin => {\n onDOMContentLoaded(() => {\n const $ = getjQuery();\n /* istanbul ignore if */\n if ($) {\n const name = plugin.NAME;\n const JQUERY_NO_CONFLICT = $.fn[name];\n $.fn[name] = plugin.jQueryInterface;\n $.fn[name].Constructor = plugin;\n $.fn[name].noConflict = () => {\n $.fn[name] = JQUERY_NO_CONFLICT;\n return plugin.jQueryInterface;\n };\n }\n });\n};\nconst execute = (possibleCallback, args = [], defaultValue = possibleCallback) => {\n return typeof possibleCallback === 'function' ? possibleCallback(...args) : defaultValue;\n};\nconst executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {\n if (!waitForTransition) {\n execute(callback);\n return;\n }\n const durationPadding = 5;\n const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding;\n let called = false;\n const handler = ({\n target\n }) => {\n if (target !== transitionElement) {\n return;\n }\n called = true;\n transitionElement.removeEventListener(TRANSITION_END, handler);\n execute(callback);\n };\n transitionElement.addEventListener(TRANSITION_END, handler);\n setTimeout(() => {\n if (!called) {\n triggerTransitionEnd(transitionElement);\n }\n }, emulatedDuration);\n};\n\n/**\n * Return the previous/next element of a list.\n *\n * @param {array} list The list of elements\n * @param activeElement The active element\n * @param shouldGetNext Choose to get next or previous element\n * @param isCycleAllowed\n * @return {Element|elem} The proper element\n */\nconst getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => {\n const listLength = list.length;\n let index = list.indexOf(activeElement);\n\n // if the element does not exist in the list return an element\n // depending on the direction and if cycle is allowed\n if (index === -1) {\n return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0];\n }\n index += shouldGetNext ? 1 : -1;\n if (isCycleAllowed) {\n index = (index + listLength) % listLength;\n }\n return list[Math.max(0, Math.min(index, listLength - 1))];\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/event-handler.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst namespaceRegex = /[^.]*(?=\\..*)\\.|.*/;\nconst stripNameRegex = /\\..*/;\nconst stripUidRegex = /::\\d+$/;\nconst eventRegistry = {}; // Events storage\nlet uidEvent = 1;\nconst customEvents = {\n mouseenter: 'mouseover',\n mouseleave: 'mouseout'\n};\nconst nativeEvents = new Set(['click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu', 'mousewheel', 'DOMMouseScroll', 'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend', 'keydown', 'keypress', 'keyup', 'orientationchange', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'pointerdown', 'pointermove', 'pointerup', 'pointerleave', 'pointercancel', 'gesturestart', 'gesturechange', 'gestureend', 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout', 'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange', 'error', 'abort', 'scroll']);\n\n/**\n * Private methods\n */\n\nfunction makeEventUid(element, uid) {\n return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++;\n}\nfunction getElementEvents(element) {\n const uid = makeEventUid(element);\n element.uidEvent = uid;\n eventRegistry[uid] = eventRegistry[uid] || {};\n return eventRegistry[uid];\n}\nfunction bootstrapHandler(element, fn) {\n return function handler(event) {\n hydrateObj(event, {\n delegateTarget: element\n });\n if (handler.oneOff) {\n EventHandler.off(element, event.type, fn);\n }\n return fn.apply(element, [event]);\n };\n}\nfunction bootstrapDelegationHandler(element, selector, fn) {\n return function handler(event) {\n const domElements = element.querySelectorAll(selector);\n for (let {\n target\n } = event; target && target !== this; target = target.parentNode) {\n for (const domElement of domElements) {\n if (domElement !== target) {\n continue;\n }\n hydrateObj(event, {\n delegateTarget: target\n });\n if (handler.oneOff) {\n EventHandler.off(element, event.type, selector, fn);\n }\n return fn.apply(target, [event]);\n }\n }\n };\n}\nfunction findHandler(events, callable, delegationSelector = null) {\n return Object.values(events).find(event => event.callable === callable && event.delegationSelector === delegationSelector);\n}\nfunction normalizeParameters(originalTypeEvent, handler, delegationFunction) {\n const isDelegated = typeof handler === 'string';\n // TODO: tooltip passes `false` instead of selector, so we need to check\n const callable = isDelegated ? delegationFunction : handler || delegationFunction;\n let typeEvent = getTypeEvent(originalTypeEvent);\n if (!nativeEvents.has(typeEvent)) {\n typeEvent = originalTypeEvent;\n }\n return [isDelegated, callable, typeEvent];\n}\nfunction addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return;\n }\n let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction);\n\n // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position\n // this prevents the handler from being dispatched the same way as mouseover or mouseout does\n if (originalTypeEvent in customEvents) {\n const wrapFunction = fn => {\n return function (event) {\n if (!event.relatedTarget || event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget)) {\n return fn.call(this, event);\n }\n };\n };\n callable = wrapFunction(callable);\n }\n const events = getElementEvents(element);\n const handlers = events[typeEvent] || (events[typeEvent] = {});\n const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null);\n if (previousFunction) {\n previousFunction.oneOff = previousFunction.oneOff && oneOff;\n return;\n }\n const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, ''));\n const fn = isDelegated ? bootstrapDelegationHandler(element, handler, callable) : bootstrapHandler(element, callable);\n fn.delegationSelector = isDelegated ? handler : null;\n fn.callable = callable;\n fn.oneOff = oneOff;\n fn.uidEvent = uid;\n handlers[uid] = fn;\n element.addEventListener(typeEvent, fn, isDelegated);\n}\nfunction removeHandler(element, events, typeEvent, handler, delegationSelector) {\n const fn = findHandler(events[typeEvent], handler, delegationSelector);\n if (!fn) {\n return;\n }\n element.removeEventListener(typeEvent, fn, Boolean(delegationSelector));\n delete events[typeEvent][fn.uidEvent];\n}\nfunction removeNamespacedHandlers(element, events, typeEvent, namespace) {\n const storeElementEvent = events[typeEvent] || {};\n for (const [handlerKey, event] of Object.entries(storeElementEvent)) {\n if (handlerKey.includes(namespace)) {\n removeHandler(element, events, typeEvent, event.callable, event.delegationSelector);\n }\n }\n}\nfunction getTypeEvent(event) {\n // allow to get the native events from namespaced events ('click.bs.button' --> 'click')\n event = event.replace(stripNameRegex, '');\n return customEvents[event] || event;\n}\nconst EventHandler = {\n on(element, event, handler, delegationFunction) {\n addHandler(element, event, handler, delegationFunction, false);\n },\n one(element, event, handler, delegationFunction) {\n addHandler(element, event, handler, delegationFunction, true);\n },\n off(element, originalTypeEvent, handler, delegationFunction) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return;\n }\n const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction);\n const inNamespace = typeEvent !== originalTypeEvent;\n const events = getElementEvents(element);\n const storeElementEvent = events[typeEvent] || {};\n const isNamespace = originalTypeEvent.startsWith('.');\n if (typeof callable !== 'undefined') {\n // Simplest case: handler is passed, remove that listener ONLY.\n if (!Object.keys(storeElementEvent).length) {\n return;\n }\n removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null);\n return;\n }\n if (isNamespace) {\n for (const elementEvent of Object.keys(events)) {\n removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1));\n }\n }\n for (const [keyHandlers, event] of Object.entries(storeElementEvent)) {\n const handlerKey = keyHandlers.replace(stripUidRegex, '');\n if (!inNamespace || originalTypeEvent.includes(handlerKey)) {\n removeHandler(element, events, typeEvent, event.callable, event.delegationSelector);\n }\n }\n },\n trigger(element, event, args) {\n if (typeof event !== 'string' || !element) {\n return null;\n }\n const $ = getjQuery();\n const typeEvent = getTypeEvent(event);\n const inNamespace = event !== typeEvent;\n let jQueryEvent = null;\n let bubbles = true;\n let nativeDispatch = true;\n let defaultPrevented = false;\n if (inNamespace && $) {\n jQueryEvent = $.Event(event, args);\n $(element).trigger(jQueryEvent);\n bubbles = !jQueryEvent.isPropagationStopped();\n nativeDispatch = !jQueryEvent.isImmediatePropagationStopped();\n defaultPrevented = jQueryEvent.isDefaultPrevented();\n }\n const evt = hydrateObj(new Event(event, {\n bubbles,\n cancelable: true\n }), args);\n if (defaultPrevented) {\n evt.preventDefault();\n }\n if (nativeDispatch) {\n element.dispatchEvent(evt);\n }\n if (evt.defaultPrevented && jQueryEvent) {\n jQueryEvent.preventDefault();\n }\n return evt;\n }\n};\nfunction hydrateObj(obj, meta = {}) {\n for (const [key, value] of Object.entries(meta)) {\n try {\n obj[key] = value;\n } catch (_unused) {\n Object.defineProperty(obj, key, {\n configurable: true,\n get() {\n return value;\n }\n });\n }\n }\n return obj;\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/manipulator.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nfunction normalizeData(value) {\n if (value === 'true') {\n return true;\n }\n if (value === 'false') {\n return false;\n }\n if (value === Number(value).toString()) {\n return Number(value);\n }\n if (value === '' || value === 'null') {\n return null;\n }\n if (typeof value !== 'string') {\n return value;\n }\n try {\n return JSON.parse(decodeURIComponent(value));\n } catch (_unused) {\n return value;\n }\n}\nfunction normalizeDataKey(key) {\n return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`);\n}\nconst Manipulator = {\n setDataAttribute(element, key, value) {\n element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value);\n },\n removeDataAttribute(element, key) {\n element.removeAttribute(`data-bs-${normalizeDataKey(key)}`);\n },\n getDataAttributes(element) {\n if (!element) {\n return {};\n }\n const attributes = {};\n const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig'));\n for (const key of bsKeys) {\n let pureKey = key.replace(/^bs/, '');\n pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1, pureKey.length);\n attributes[pureKey] = normalizeData(element.dataset[key]);\n }\n return attributes;\n },\n getDataAttribute(element, key) {\n return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`));\n }\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/config.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Class definition\n */\n\nclass Config {\n // Getters\n static get Default() {\n return {};\n }\n static get DefaultType() {\n return {};\n }\n static get NAME() {\n throw new Error('You have to implement the static method \"NAME\", for each component!');\n }\n _getConfig(config) {\n config = this._mergeConfigObj(config);\n config = this._configAfterMerge(config);\n this._typeCheckConfig(config);\n return config;\n }\n _configAfterMerge(config) {\n return config;\n }\n _mergeConfigObj(config, element) {\n const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {}; // try to parse\n\n return {\n ...this.constructor.Default,\n ...(typeof jsonConfig === 'object' ? jsonConfig : {}),\n ...(isElement(element) ? Manipulator.getDataAttributes(element) : {}),\n ...(typeof config === 'object' ? config : {})\n };\n }\n _typeCheckConfig(config, configTypes = this.constructor.DefaultType) {\n for (const [property, expectedTypes] of Object.entries(configTypes)) {\n const value = config[property];\n const valueType = isElement(value) ? 'element' : toType(value);\n if (!new RegExp(expectedTypes).test(valueType)) {\n throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option \"${property}\" provided type \"${valueType}\" but expected type \"${expectedTypes}\".`);\n }\n }\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap base-component.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst VERSION = '5.3.2';\n\n/**\n * Class definition\n */\n\nclass BaseComponent extends Config {\n constructor(element, config) {\n super();\n element = getElement(element);\n if (!element) {\n return;\n }\n this._element = element;\n this._config = this._getConfig(config);\n Data.set(this._element, this.constructor.DATA_KEY, this);\n }\n\n // Public\n dispose() {\n Data.remove(this._element, this.constructor.DATA_KEY);\n EventHandler.off(this._element, this.constructor.EVENT_KEY);\n for (const propertyName of Object.getOwnPropertyNames(this)) {\n this[propertyName] = null;\n }\n }\n _queueCallback(callback, element, isAnimated = true) {\n executeAfterTransition(callback, element, isAnimated);\n }\n _getConfig(config) {\n config = this._mergeConfigObj(config, this._element);\n config = this._configAfterMerge(config);\n this._typeCheckConfig(config);\n return config;\n }\n\n // Static\n static getInstance(element) {\n return Data.get(getElement(element), this.DATA_KEY);\n }\n static getOrCreateInstance(element, config = {}) {\n return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null);\n }\n static get VERSION() {\n return VERSION;\n }\n static get DATA_KEY() {\n return `bs.${this.NAME}`;\n }\n static get EVENT_KEY() {\n return `.${this.DATA_KEY}`;\n }\n static eventName(name) {\n return `${name}${this.EVENT_KEY}`;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/selector-engine.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst getSelector = element => {\n let selector = element.getAttribute('data-bs-target');\n if (!selector || selector === '#') {\n let hrefAttribute = element.getAttribute('href');\n\n // The only valid content that could double as a selector are IDs or classes,\n // so everything starting with `#` or `.`. If a \"real\" URL is used as the selector,\n // `document.querySelector` will rightfully complain it is invalid.\n // See https://github.com/twbs/bootstrap/issues/32273\n if (!hrefAttribute || !hrefAttribute.includes('#') && !hrefAttribute.startsWith('.')) {\n return null;\n }\n\n // Just in case some CMS puts out a full URL with the anchor appended\n if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) {\n hrefAttribute = `#${hrefAttribute.split('#')[1]}`;\n }\n selector = hrefAttribute && hrefAttribute !== '#' ? parseSelector(hrefAttribute.trim()) : null;\n }\n return selector;\n};\nconst SelectorEngine = {\n find(selector, element = document.documentElement) {\n return [].concat(...Element.prototype.querySelectorAll.call(element, selector));\n },\n findOne(selector, element = document.documentElement) {\n return Element.prototype.querySelector.call(element, selector);\n },\n children(element, selector) {\n return [].concat(...element.children).filter(child => child.matches(selector));\n },\n parents(element, selector) {\n const parents = [];\n let ancestor = element.parentNode.closest(selector);\n while (ancestor) {\n parents.push(ancestor);\n ancestor = ancestor.parentNode.closest(selector);\n }\n return parents;\n },\n prev(element, selector) {\n let previous = element.previousElementSibling;\n while (previous) {\n if (previous.matches(selector)) {\n return [previous];\n }\n previous = previous.previousElementSibling;\n }\n return [];\n },\n // TODO: this is now unused; remove later along with prev()\n next(element, selector) {\n let next = element.nextElementSibling;\n while (next) {\n if (next.matches(selector)) {\n return [next];\n }\n next = next.nextElementSibling;\n }\n return [];\n },\n focusableChildren(element) {\n const focusables = ['a', 'button', 'input', 'textarea', 'select', 'details', '[tabindex]', '[contenteditable=\"true\"]'].map(selector => `${selector}:not([tabindex^=\"-\"])`).join(',');\n return this.find(focusables, element).filter(el => !isDisabled(el) && isVisible(el));\n },\n getSelectorFromElement(element) {\n const selector = getSelector(element);\n if (selector) {\n return SelectorEngine.findOne(selector) ? selector : null;\n }\n return null;\n },\n getElementFromSelector(element) {\n const selector = getSelector(element);\n return selector ? SelectorEngine.findOne(selector) : null;\n },\n getMultipleElementsFromSelector(element) {\n const selector = getSelector(element);\n return selector ? SelectorEngine.find(selector) : [];\n }\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/component-functions.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst enableDismissTrigger = (component, method = 'hide') => {\n const clickEvent = `click.dismiss${component.EVENT_KEY}`;\n const name = component.NAME;\n EventHandler.on(document, clickEvent, `[data-bs-dismiss=\"${name}\"]`, function (event) {\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n if (isDisabled(this)) {\n return;\n }\n const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`);\n const instance = component.getOrCreateInstance(target);\n\n // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method\n instance[method]();\n });\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap alert.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$f = 'alert';\nconst DATA_KEY$a = 'bs.alert';\nconst EVENT_KEY$b = `.${DATA_KEY$a}`;\nconst EVENT_CLOSE = `close${EVENT_KEY$b}`;\nconst EVENT_CLOSED = `closed${EVENT_KEY$b}`;\nconst CLASS_NAME_FADE$5 = 'fade';\nconst CLASS_NAME_SHOW$8 = 'show';\n\n/**\n * Class definition\n */\n\nclass Alert extends BaseComponent {\n // Getters\n static get NAME() {\n return NAME$f;\n }\n\n // Public\n close() {\n const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE);\n if (closeEvent.defaultPrevented) {\n return;\n }\n this._element.classList.remove(CLASS_NAME_SHOW$8);\n const isAnimated = this._element.classList.contains(CLASS_NAME_FADE$5);\n this._queueCallback(() => this._destroyElement(), this._element, isAnimated);\n }\n\n // Private\n _destroyElement() {\n this._element.remove();\n EventHandler.trigger(this._element, EVENT_CLOSED);\n this.dispose();\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Alert.getOrCreateInstance(this);\n if (typeof config !== 'string') {\n return;\n }\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config](this);\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nenableDismissTrigger(Alert, 'close');\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Alert);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap button.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$e = 'button';\nconst DATA_KEY$9 = 'bs.button';\nconst EVENT_KEY$a = `.${DATA_KEY$9}`;\nconst DATA_API_KEY$6 = '.data-api';\nconst CLASS_NAME_ACTIVE$3 = 'active';\nconst SELECTOR_DATA_TOGGLE$5 = '[data-bs-toggle=\"button\"]';\nconst EVENT_CLICK_DATA_API$6 = `click${EVENT_KEY$a}${DATA_API_KEY$6}`;\n\n/**\n * Class definition\n */\n\nclass Button extends BaseComponent {\n // Getters\n static get NAME() {\n return NAME$e;\n }\n\n // Public\n toggle() {\n // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method\n this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE$3));\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Button.getOrCreateInstance(this);\n if (config === 'toggle') {\n data[config]();\n }\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$6, SELECTOR_DATA_TOGGLE$5, event => {\n event.preventDefault();\n const button = event.target.closest(SELECTOR_DATA_TOGGLE$5);\n const data = Button.getOrCreateInstance(button);\n data.toggle();\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Button);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/swipe.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$d = 'swipe';\nconst EVENT_KEY$9 = '.bs.swipe';\nconst EVENT_TOUCHSTART = `touchstart${EVENT_KEY$9}`;\nconst EVENT_TOUCHMOVE = `touchmove${EVENT_KEY$9}`;\nconst EVENT_TOUCHEND = `touchend${EVENT_KEY$9}`;\nconst EVENT_POINTERDOWN = `pointerdown${EVENT_KEY$9}`;\nconst EVENT_POINTERUP = `pointerup${EVENT_KEY$9}`;\nconst POINTER_TYPE_TOUCH = 'touch';\nconst POINTER_TYPE_PEN = 'pen';\nconst CLASS_NAME_POINTER_EVENT = 'pointer-event';\nconst SWIPE_THRESHOLD = 40;\nconst Default$c = {\n endCallback: null,\n leftCallback: null,\n rightCallback: null\n};\nconst DefaultType$c = {\n endCallback: '(function|null)',\n leftCallback: '(function|null)',\n rightCallback: '(function|null)'\n};\n\n/**\n * Class definition\n */\n\nclass Swipe extends Config {\n constructor(element, config) {\n super();\n this._element = element;\n if (!element || !Swipe.isSupported()) {\n return;\n }\n this._config = this._getConfig(config);\n this._deltaX = 0;\n this._supportPointerEvents = Boolean(window.PointerEvent);\n this._initEvents();\n }\n\n // Getters\n static get Default() {\n return Default$c;\n }\n static get DefaultType() {\n return DefaultType$c;\n }\n static get NAME() {\n return NAME$d;\n }\n\n // Public\n dispose() {\n EventHandler.off(this._element, EVENT_KEY$9);\n }\n\n // Private\n _start(event) {\n if (!this._supportPointerEvents) {\n this._deltaX = event.touches[0].clientX;\n return;\n }\n if (this._eventIsPointerPenTouch(event)) {\n this._deltaX = event.clientX;\n }\n }\n _end(event) {\n if (this._eventIsPointerPenTouch(event)) {\n this._deltaX = event.clientX - this._deltaX;\n }\n this._handleSwipe();\n execute(this._config.endCallback);\n }\n _move(event) {\n this._deltaX = event.touches && event.touches.length > 1 ? 0 : event.touches[0].clientX - this._deltaX;\n }\n _handleSwipe() {\n const absDeltaX = Math.abs(this._deltaX);\n if (absDeltaX <= SWIPE_THRESHOLD) {\n return;\n }\n const direction = absDeltaX / this._deltaX;\n this._deltaX = 0;\n if (!direction) {\n return;\n }\n execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback);\n }\n _initEvents() {\n if (this._supportPointerEvents) {\n EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event));\n EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event));\n this._element.classList.add(CLASS_NAME_POINTER_EVENT);\n } else {\n EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event));\n EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event));\n EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event));\n }\n }\n _eventIsPointerPenTouch(event) {\n return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH);\n }\n\n // Static\n static isSupported() {\n return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap carousel.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$c = 'carousel';\nconst DATA_KEY$8 = 'bs.carousel';\nconst EVENT_KEY$8 = `.${DATA_KEY$8}`;\nconst DATA_API_KEY$5 = '.data-api';\nconst ARROW_LEFT_KEY$1 = 'ArrowLeft';\nconst ARROW_RIGHT_KEY$1 = 'ArrowRight';\nconst TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch\n\nconst ORDER_NEXT = 'next';\nconst ORDER_PREV = 'prev';\nconst DIRECTION_LEFT = 'left';\nconst DIRECTION_RIGHT = 'right';\nconst EVENT_SLIDE = `slide${EVENT_KEY$8}`;\nconst EVENT_SLID = `slid${EVENT_KEY$8}`;\nconst EVENT_KEYDOWN$1 = `keydown${EVENT_KEY$8}`;\nconst EVENT_MOUSEENTER$1 = `mouseenter${EVENT_KEY$8}`;\nconst EVENT_MOUSELEAVE$1 = `mouseleave${EVENT_KEY$8}`;\nconst EVENT_DRAG_START = `dragstart${EVENT_KEY$8}`;\nconst EVENT_LOAD_DATA_API$3 = `load${EVENT_KEY$8}${DATA_API_KEY$5}`;\nconst EVENT_CLICK_DATA_API$5 = `click${EVENT_KEY$8}${DATA_API_KEY$5}`;\nconst CLASS_NAME_CAROUSEL = 'carousel';\nconst CLASS_NAME_ACTIVE$2 = 'active';\nconst CLASS_NAME_SLIDE = 'slide';\nconst CLASS_NAME_END = 'carousel-item-end';\nconst CLASS_NAME_START = 'carousel-item-start';\nconst CLASS_NAME_NEXT = 'carousel-item-next';\nconst CLASS_NAME_PREV = 'carousel-item-prev';\nconst SELECTOR_ACTIVE = '.active';\nconst SELECTOR_ITEM = '.carousel-item';\nconst SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM;\nconst SELECTOR_ITEM_IMG = '.carousel-item img';\nconst SELECTOR_INDICATORS = '.carousel-indicators';\nconst SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]';\nconst SELECTOR_DATA_RIDE = '[data-bs-ride=\"carousel\"]';\nconst KEY_TO_DIRECTION = {\n [ARROW_LEFT_KEY$1]: DIRECTION_RIGHT,\n [ARROW_RIGHT_KEY$1]: DIRECTION_LEFT\n};\nconst Default$b = {\n interval: 5000,\n keyboard: true,\n pause: 'hover',\n ride: false,\n touch: true,\n wrap: true\n};\nconst DefaultType$b = {\n interval: '(number|boolean)',\n // TODO:v6 remove boolean support\n keyboard: 'boolean',\n pause: '(string|boolean)',\n ride: '(boolean|string)',\n touch: 'boolean',\n wrap: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Carousel extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._interval = null;\n this._activeElement = null;\n this._isSliding = false;\n this.touchTimeout = null;\n this._swipeHelper = null;\n this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element);\n this._addEventListeners();\n if (this._config.ride === CLASS_NAME_CAROUSEL) {\n this.cycle();\n }\n }\n\n // Getters\n static get Default() {\n return Default$b;\n }\n static get DefaultType() {\n return DefaultType$b;\n }\n static get NAME() {\n return NAME$c;\n }\n\n // Public\n next() {\n this._slide(ORDER_NEXT);\n }\n nextWhenVisible() {\n // FIXME TODO use `document.visibilityState`\n // Don't call next when the page isn't visible\n // or the carousel or its parent isn't visible\n if (!document.hidden && isVisible(this._element)) {\n this.next();\n }\n }\n prev() {\n this._slide(ORDER_PREV);\n }\n pause() {\n if (this._isSliding) {\n triggerTransitionEnd(this._element);\n }\n this._clearInterval();\n }\n cycle() {\n this._clearInterval();\n this._updateInterval();\n this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval);\n }\n _maybeEnableCycle() {\n if (!this._config.ride) {\n return;\n }\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.cycle());\n return;\n }\n this.cycle();\n }\n to(index) {\n const items = this._getItems();\n if (index > items.length - 1 || index < 0) {\n return;\n }\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.to(index));\n return;\n }\n const activeIndex = this._getItemIndex(this._getActive());\n if (activeIndex === index) {\n return;\n }\n const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV;\n this._slide(order, items[index]);\n }\n dispose() {\n if (this._swipeHelper) {\n this._swipeHelper.dispose();\n }\n super.dispose();\n }\n\n // Private\n _configAfterMerge(config) {\n config.defaultInterval = config.interval;\n return config;\n }\n _addEventListeners() {\n if (this._config.keyboard) {\n EventHandler.on(this._element, EVENT_KEYDOWN$1, event => this._keydown(event));\n }\n if (this._config.pause === 'hover') {\n EventHandler.on(this._element, EVENT_MOUSEENTER$1, () => this.pause());\n EventHandler.on(this._element, EVENT_MOUSELEAVE$1, () => this._maybeEnableCycle());\n }\n if (this._config.touch && Swipe.isSupported()) {\n this._addTouchEventListeners();\n }\n }\n _addTouchEventListeners() {\n for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {\n EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault());\n }\n const endCallBack = () => {\n if (this._config.pause !== 'hover') {\n return;\n }\n\n // If it's a touch-enabled device, mouseenter/leave are fired as\n // part of the mouse compatibility events on first tap - the carousel\n // would stop cycling until user tapped out of it;\n // here, we listen for touchend, explicitly pause the carousel\n // (as if it's the second time we tap on it, mouseenter compat event\n // is NOT fired) and after a timeout (to allow for mouse compatibility\n // events to fire) we explicitly restart cycling\n\n this.pause();\n if (this.touchTimeout) {\n clearTimeout(this.touchTimeout);\n }\n this.touchTimeout = setTimeout(() => this._maybeEnableCycle(), TOUCHEVENT_COMPAT_WAIT + this._config.interval);\n };\n const swipeConfig = {\n leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)),\n rightCallback: () => this._slide(this._directionToOrder(DIRECTION_RIGHT)),\n endCallback: endCallBack\n };\n this._swipeHelper = new Swipe(this._element, swipeConfig);\n }\n _keydown(event) {\n if (/input|textarea/i.test(event.target.tagName)) {\n return;\n }\n const direction = KEY_TO_DIRECTION[event.key];\n if (direction) {\n event.preventDefault();\n this._slide(this._directionToOrder(direction));\n }\n }\n _getItemIndex(element) {\n return this._getItems().indexOf(element);\n }\n _setActiveIndicatorElement(index) {\n if (!this._indicatorsElement) {\n return;\n }\n const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement);\n activeIndicator.classList.remove(CLASS_NAME_ACTIVE$2);\n activeIndicator.removeAttribute('aria-current');\n const newActiveIndicator = SelectorEngine.findOne(`[data-bs-slide-to=\"${index}\"]`, this._indicatorsElement);\n if (newActiveIndicator) {\n newActiveIndicator.classList.add(CLASS_NAME_ACTIVE$2);\n newActiveIndicator.setAttribute('aria-current', 'true');\n }\n }\n _updateInterval() {\n const element = this._activeElement || this._getActive();\n if (!element) {\n return;\n }\n const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10);\n this._config.interval = elementInterval || this._config.defaultInterval;\n }\n _slide(order, element = null) {\n if (this._isSliding) {\n return;\n }\n const activeElement = this._getActive();\n const isNext = order === ORDER_NEXT;\n const nextElement = element || getNextActiveElement(this._getItems(), activeElement, isNext, this._config.wrap);\n if (nextElement === activeElement) {\n return;\n }\n const nextElementIndex = this._getItemIndex(nextElement);\n const triggerEvent = eventName => {\n return EventHandler.trigger(this._element, eventName, {\n relatedTarget: nextElement,\n direction: this._orderToDirection(order),\n from: this._getItemIndex(activeElement),\n to: nextElementIndex\n });\n };\n const slideEvent = triggerEvent(EVENT_SLIDE);\n if (slideEvent.defaultPrevented) {\n return;\n }\n if (!activeElement || !nextElement) {\n // Some weirdness is happening, so we bail\n // TODO: change tests that use empty divs to avoid this check\n return;\n }\n const isCycling = Boolean(this._interval);\n this.pause();\n this._isSliding = true;\n this._setActiveIndicatorElement(nextElementIndex);\n this._activeElement = nextElement;\n const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END;\n const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV;\n nextElement.classList.add(orderClassName);\n reflow(nextElement);\n activeElement.classList.add(directionalClassName);\n nextElement.classList.add(directionalClassName);\n const completeCallBack = () => {\n nextElement.classList.remove(directionalClassName, orderClassName);\n nextElement.classList.add(CLASS_NAME_ACTIVE$2);\n activeElement.classList.remove(CLASS_NAME_ACTIVE$2, orderClassName, directionalClassName);\n this._isSliding = false;\n triggerEvent(EVENT_SLID);\n };\n this._queueCallback(completeCallBack, activeElement, this._isAnimated());\n if (isCycling) {\n this.cycle();\n }\n }\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_SLIDE);\n }\n _getActive() {\n return SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element);\n }\n _getItems() {\n return SelectorEngine.find(SELECTOR_ITEM, this._element);\n }\n _clearInterval() {\n if (this._interval) {\n clearInterval(this._interval);\n this._interval = null;\n }\n }\n _directionToOrder(direction) {\n if (isRTL()) {\n return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT;\n }\n return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV;\n }\n _orderToDirection(order) {\n if (isRTL()) {\n return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT;\n }\n return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT;\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Carousel.getOrCreateInstance(this, config);\n if (typeof config === 'number') {\n data.to(config);\n return;\n }\n if (typeof config === 'string') {\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n }\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$5, SELECTOR_DATA_SLIDE, function (event) {\n const target = SelectorEngine.getElementFromSelector(this);\n if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) {\n return;\n }\n event.preventDefault();\n const carousel = Carousel.getOrCreateInstance(target);\n const slideIndex = this.getAttribute('data-bs-slide-to');\n if (slideIndex) {\n carousel.to(slideIndex);\n carousel._maybeEnableCycle();\n return;\n }\n if (Manipulator.getDataAttribute(this, 'slide') === 'next') {\n carousel.next();\n carousel._maybeEnableCycle();\n return;\n }\n carousel.prev();\n carousel._maybeEnableCycle();\n});\nEventHandler.on(window, EVENT_LOAD_DATA_API$3, () => {\n const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE);\n for (const carousel of carousels) {\n Carousel.getOrCreateInstance(carousel);\n }\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Carousel);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap collapse.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$b = 'collapse';\nconst DATA_KEY$7 = 'bs.collapse';\nconst EVENT_KEY$7 = `.${DATA_KEY$7}`;\nconst DATA_API_KEY$4 = '.data-api';\nconst EVENT_SHOW$6 = `show${EVENT_KEY$7}`;\nconst EVENT_SHOWN$6 = `shown${EVENT_KEY$7}`;\nconst EVENT_HIDE$6 = `hide${EVENT_KEY$7}`;\nconst EVENT_HIDDEN$6 = `hidden${EVENT_KEY$7}`;\nconst EVENT_CLICK_DATA_API$4 = `click${EVENT_KEY$7}${DATA_API_KEY$4}`;\nconst CLASS_NAME_SHOW$7 = 'show';\nconst CLASS_NAME_COLLAPSE = 'collapse';\nconst CLASS_NAME_COLLAPSING = 'collapsing';\nconst CLASS_NAME_COLLAPSED = 'collapsed';\nconst CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`;\nconst CLASS_NAME_HORIZONTAL = 'collapse-horizontal';\nconst WIDTH = 'width';\nconst HEIGHT = 'height';\nconst SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing';\nconst SELECTOR_DATA_TOGGLE$4 = '[data-bs-toggle=\"collapse\"]';\nconst Default$a = {\n parent: null,\n toggle: true\n};\nconst DefaultType$a = {\n parent: '(null|element)',\n toggle: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Collapse extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._isTransitioning = false;\n this._triggerArray = [];\n const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE$4);\n for (const elem of toggleList) {\n const selector = SelectorEngine.getSelectorFromElement(elem);\n const filterElement = SelectorEngine.find(selector).filter(foundElement => foundElement === this._element);\n if (selector !== null && filterElement.length) {\n this._triggerArray.push(elem);\n }\n }\n this._initializeChildren();\n if (!this._config.parent) {\n this._addAriaAndCollapsedClass(this._triggerArray, this._isShown());\n }\n if (this._config.toggle) {\n this.toggle();\n }\n }\n\n // Getters\n static get Default() {\n return Default$a;\n }\n static get DefaultType() {\n return DefaultType$a;\n }\n static get NAME() {\n return NAME$b;\n }\n\n // Public\n toggle() {\n if (this._isShown()) {\n this.hide();\n } else {\n this.show();\n }\n }\n show() {\n if (this._isTransitioning || this._isShown()) {\n return;\n }\n let activeChildren = [];\n\n // find active children\n if (this._config.parent) {\n activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(element => element !== this._element).map(element => Collapse.getOrCreateInstance(element, {\n toggle: false\n }));\n }\n if (activeChildren.length && activeChildren[0]._isTransitioning) {\n return;\n }\n const startEvent = EventHandler.trigger(this._element, EVENT_SHOW$6);\n if (startEvent.defaultPrevented) {\n return;\n }\n for (const activeInstance of activeChildren) {\n activeInstance.hide();\n }\n const dimension = this._getDimension();\n this._element.classList.remove(CLASS_NAME_COLLAPSE);\n this._element.classList.add(CLASS_NAME_COLLAPSING);\n this._element.style[dimension] = 0;\n this._addAriaAndCollapsedClass(this._triggerArray, true);\n this._isTransitioning = true;\n const complete = () => {\n this._isTransitioning = false;\n this._element.classList.remove(CLASS_NAME_COLLAPSING);\n this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$7);\n this._element.style[dimension] = '';\n EventHandler.trigger(this._element, EVENT_SHOWN$6);\n };\n const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1);\n const scrollSize = `scroll${capitalizedDimension}`;\n this._queueCallback(complete, this._element, true);\n this._element.style[dimension] = `${this._element[scrollSize]}px`;\n }\n hide() {\n if (this._isTransitioning || !this._isShown()) {\n return;\n }\n const startEvent = EventHandler.trigger(this._element, EVENT_HIDE$6);\n if (startEvent.defaultPrevented) {\n return;\n }\n const dimension = this._getDimension();\n this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`;\n reflow(this._element);\n this._element.classList.add(CLASS_NAME_COLLAPSING);\n this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$7);\n for (const trigger of this._triggerArray) {\n const element = SelectorEngine.getElementFromSelector(trigger);\n if (element && !this._isShown(element)) {\n this._addAriaAndCollapsedClass([trigger], false);\n }\n }\n this._isTransitioning = true;\n const complete = () => {\n this._isTransitioning = false;\n this._element.classList.remove(CLASS_NAME_COLLAPSING);\n this._element.classList.add(CLASS_NAME_COLLAPSE);\n EventHandler.trigger(this._element, EVENT_HIDDEN$6);\n };\n this._element.style[dimension] = '';\n this._queueCallback(complete, this._element, true);\n }\n _isShown(element = this._element) {\n return element.classList.contains(CLASS_NAME_SHOW$7);\n }\n\n // Private\n _configAfterMerge(config) {\n config.toggle = Boolean(config.toggle); // Coerce string values\n config.parent = getElement(config.parent);\n return config;\n }\n _getDimension() {\n return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT;\n }\n _initializeChildren() {\n if (!this._config.parent) {\n return;\n }\n const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE$4);\n for (const element of children) {\n const selected = SelectorEngine.getElementFromSelector(element);\n if (selected) {\n this._addAriaAndCollapsedClass([element], this._isShown(selected));\n }\n }\n }\n _getFirstLevelChildren(selector) {\n const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent);\n // remove children if greater depth\n return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element));\n }\n _addAriaAndCollapsedClass(triggerArray, isOpen) {\n if (!triggerArray.length) {\n return;\n }\n for (const element of triggerArray) {\n element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen);\n element.setAttribute('aria-expanded', isOpen);\n }\n }\n\n // Static\n static jQueryInterface(config) {\n const _config = {};\n if (typeof config === 'string' && /show|hide/.test(config)) {\n _config.toggle = false;\n }\n return this.each(function () {\n const data = Collapse.getOrCreateInstance(this, _config);\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n }\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$4, SELECTOR_DATA_TOGGLE$4, function (event) {\n // preventDefault only for elements (which change the URL) not inside the collapsible element\n if (event.target.tagName === 'A' || event.delegateTarget && event.delegateTarget.tagName === 'A') {\n event.preventDefault();\n }\n for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) {\n Collapse.getOrCreateInstance(element, {\n toggle: false\n }).toggle();\n }\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Collapse);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dropdown.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$a = 'dropdown';\nconst DATA_KEY$6 = 'bs.dropdown';\nconst EVENT_KEY$6 = `.${DATA_KEY$6}`;\nconst DATA_API_KEY$3 = '.data-api';\nconst ESCAPE_KEY$2 = 'Escape';\nconst TAB_KEY$1 = 'Tab';\nconst ARROW_UP_KEY$1 = 'ArrowUp';\nconst ARROW_DOWN_KEY$1 = 'ArrowDown';\nconst RIGHT_MOUSE_BUTTON = 2; // MouseEvent.button value for the secondary button, usually the right button\n\nconst EVENT_HIDE$5 = `hide${EVENT_KEY$6}`;\nconst EVENT_HIDDEN$5 = `hidden${EVENT_KEY$6}`;\nconst EVENT_SHOW$5 = `show${EVENT_KEY$6}`;\nconst EVENT_SHOWN$5 = `shown${EVENT_KEY$6}`;\nconst EVENT_CLICK_DATA_API$3 = `click${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst CLASS_NAME_SHOW$6 = 'show';\nconst CLASS_NAME_DROPUP = 'dropup';\nconst CLASS_NAME_DROPEND = 'dropend';\nconst CLASS_NAME_DROPSTART = 'dropstart';\nconst CLASS_NAME_DROPUP_CENTER = 'dropup-center';\nconst CLASS_NAME_DROPDOWN_CENTER = 'dropdown-center';\nconst SELECTOR_DATA_TOGGLE$3 = '[data-bs-toggle=\"dropdown\"]:not(.disabled):not(:disabled)';\nconst SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE$3}.${CLASS_NAME_SHOW$6}`;\nconst SELECTOR_MENU = '.dropdown-menu';\nconst SELECTOR_NAVBAR = '.navbar';\nconst SELECTOR_NAVBAR_NAV = '.navbar-nav';\nconst SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)';\nconst PLACEMENT_TOP = isRTL() ? 'top-end' : 'top-start';\nconst PLACEMENT_TOPEND = isRTL() ? 'top-start' : 'top-end';\nconst PLACEMENT_BOTTOM = isRTL() ? 'bottom-end' : 'bottom-start';\nconst PLACEMENT_BOTTOMEND = isRTL() ? 'bottom-start' : 'bottom-end';\nconst PLACEMENT_RIGHT = isRTL() ? 'left-start' : 'right-start';\nconst PLACEMENT_LEFT = isRTL() ? 'right-start' : 'left-start';\nconst PLACEMENT_TOPCENTER = 'top';\nconst PLACEMENT_BOTTOMCENTER = 'bottom';\nconst Default$9 = {\n autoClose: true,\n boundary: 'clippingParents',\n display: 'dynamic',\n offset: [0, 2],\n popperConfig: null,\n reference: 'toggle'\n};\nconst DefaultType$9 = {\n autoClose: '(boolean|string)',\n boundary: '(string|element)',\n display: 'string',\n offset: '(array|string|function)',\n popperConfig: '(null|object|function)',\n reference: '(string|element|object)'\n};\n\n/**\n * Class definition\n */\n\nclass Dropdown extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._popper = null;\n this._parent = this._element.parentNode; // dropdown wrapper\n // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n this._menu = SelectorEngine.next(this._element, SELECTOR_MENU)[0] || SelectorEngine.prev(this._element, SELECTOR_MENU)[0] || SelectorEngine.findOne(SELECTOR_MENU, this._parent);\n this._inNavbar = this._detectNavbar();\n }\n\n // Getters\n static get Default() {\n return Default$9;\n }\n static get DefaultType() {\n return DefaultType$9;\n }\n static get NAME() {\n return NAME$a;\n }\n\n // Public\n toggle() {\n return this._isShown() ? this.hide() : this.show();\n }\n show() {\n if (isDisabled(this._element) || this._isShown()) {\n return;\n }\n const relatedTarget = {\n relatedTarget: this._element\n };\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$5, relatedTarget);\n if (showEvent.defaultPrevented) {\n return;\n }\n this._createPopper();\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.on(element, 'mouseover', noop);\n }\n }\n this._element.focus();\n this._element.setAttribute('aria-expanded', true);\n this._menu.classList.add(CLASS_NAME_SHOW$6);\n this._element.classList.add(CLASS_NAME_SHOW$6);\n EventHandler.trigger(this._element, EVENT_SHOWN$5, relatedTarget);\n }\n hide() {\n if (isDisabled(this._element) || !this._isShown()) {\n return;\n }\n const relatedTarget = {\n relatedTarget: this._element\n };\n this._completeHide(relatedTarget);\n }\n dispose() {\n if (this._popper) {\n this._popper.destroy();\n }\n super.dispose();\n }\n update() {\n this._inNavbar = this._detectNavbar();\n if (this._popper) {\n this._popper.update();\n }\n }\n\n // Private\n _completeHide(relatedTarget) {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$5, relatedTarget);\n if (hideEvent.defaultPrevented) {\n return;\n }\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.off(element, 'mouseover', noop);\n }\n }\n if (this._popper) {\n this._popper.destroy();\n }\n this._menu.classList.remove(CLASS_NAME_SHOW$6);\n this._element.classList.remove(CLASS_NAME_SHOW$6);\n this._element.setAttribute('aria-expanded', 'false');\n Manipulator.removeDataAttribute(this._menu, 'popper');\n EventHandler.trigger(this._element, EVENT_HIDDEN$5, relatedTarget);\n }\n _getConfig(config) {\n config = super._getConfig(config);\n if (typeof config.reference === 'object' && !isElement(config.reference) && typeof config.reference.getBoundingClientRect !== 'function') {\n // Popper virtual elements require a getBoundingClientRect method\n throw new TypeError(`${NAME$a.toUpperCase()}: Option \"reference\" provided type \"object\" without a required \"getBoundingClientRect\" method.`);\n }\n return config;\n }\n _createPopper() {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s dropdowns require Popper (https://popper.js.org)');\n }\n let referenceElement = this._element;\n if (this._config.reference === 'parent') {\n referenceElement = this._parent;\n } else if (isElement(this._config.reference)) {\n referenceElement = getElement(this._config.reference);\n } else if (typeof this._config.reference === 'object') {\n referenceElement = this._config.reference;\n }\n const popperConfig = this._getPopperConfig();\n this._popper = Popper.createPopper(referenceElement, this._menu, popperConfig);\n }\n _isShown() {\n return this._menu.classList.contains(CLASS_NAME_SHOW$6);\n }\n _getPlacement() {\n const parentDropdown = this._parent;\n if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) {\n return PLACEMENT_RIGHT;\n }\n if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) {\n return PLACEMENT_LEFT;\n }\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP_CENTER)) {\n return PLACEMENT_TOPCENTER;\n }\n if (parentDropdown.classList.contains(CLASS_NAME_DROPDOWN_CENTER)) {\n return PLACEMENT_BOTTOMCENTER;\n }\n\n // We need to trim the value because custom properties can also include spaces\n const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end';\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP)) {\n return isEnd ? PLACEMENT_TOPEND : PLACEMENT_TOP;\n }\n return isEnd ? PLACEMENT_BOTTOMEND : PLACEMENT_BOTTOM;\n }\n _detectNavbar() {\n return this._element.closest(SELECTOR_NAVBAR) !== null;\n }\n _getOffset() {\n const {\n offset\n } = this._config;\n if (typeof offset === 'string') {\n return offset.split(',').map(value => Number.parseInt(value, 10));\n }\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element);\n }\n return offset;\n }\n _getPopperConfig() {\n const defaultBsPopperConfig = {\n placement: this._getPlacement(),\n modifiers: [{\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n }, {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n }]\n };\n\n // Disable Popper if we have a static display or Dropdown is in Navbar\n if (this._inNavbar || this._config.display === 'static') {\n Manipulator.setDataAttribute(this._menu, 'popper', 'static'); // TODO: v6 remove\n defaultBsPopperConfig.modifiers = [{\n name: 'applyStyles',\n enabled: false\n }];\n }\n return {\n ...defaultBsPopperConfig,\n ...execute(this._config.popperConfig, [defaultBsPopperConfig])\n };\n }\n _selectMenuItem({\n key,\n target\n }) {\n const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => isVisible(element));\n if (!items.length) {\n return;\n }\n\n // if target isn't included in items (e.g. when expanding the dropdown)\n // allow cycling to get the last item in case key equals ARROW_UP_KEY\n getNextActiveElement(items, target, key === ARROW_DOWN_KEY$1, !items.includes(target)).focus();\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Dropdown.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n });\n }\n static clearMenus(event) {\n if (event.button === RIGHT_MOUSE_BUTTON || event.type === 'keyup' && event.key !== TAB_KEY$1) {\n return;\n }\n const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN);\n for (const toggle of openToggles) {\n const context = Dropdown.getInstance(toggle);\n if (!context || context._config.autoClose === false) {\n continue;\n }\n const composedPath = event.composedPath();\n const isMenuTarget = composedPath.includes(context._menu);\n if (composedPath.includes(context._element) || context._config.autoClose === 'inside' && !isMenuTarget || context._config.autoClose === 'outside' && isMenuTarget) {\n continue;\n }\n\n // Tab navigation through the dropdown menu or events from contained inputs shouldn't close the menu\n if (context._menu.contains(event.target) && (event.type === 'keyup' && event.key === TAB_KEY$1 || /input|select|option|textarea|form/i.test(event.target.tagName))) {\n continue;\n }\n const relatedTarget = {\n relatedTarget: context._element\n };\n if (event.type === 'click') {\n relatedTarget.clickEvent = event;\n }\n context._completeHide(relatedTarget);\n }\n }\n static dataApiKeydownHandler(event) {\n // If not an UP | DOWN | ESCAPE key => not a dropdown command\n // If input/textarea && if key is other than ESCAPE => not a dropdown command\n\n const isInput = /input|textarea/i.test(event.target.tagName);\n const isEscapeEvent = event.key === ESCAPE_KEY$2;\n const isUpOrDownEvent = [ARROW_UP_KEY$1, ARROW_DOWN_KEY$1].includes(event.key);\n if (!isUpOrDownEvent && !isEscapeEvent) {\n return;\n }\n if (isInput && !isEscapeEvent) {\n return;\n }\n event.preventDefault();\n\n // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE$3) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE$3)[0] || SelectorEngine.next(this, SELECTOR_DATA_TOGGLE$3)[0] || SelectorEngine.findOne(SELECTOR_DATA_TOGGLE$3, event.delegateTarget.parentNode);\n const instance = Dropdown.getOrCreateInstance(getToggleButton);\n if (isUpOrDownEvent) {\n event.stopPropagation();\n instance.show();\n instance._selectMenuItem(event);\n return;\n }\n if (instance._isShown()) {\n // else is escape and we check if it is shown\n event.stopPropagation();\n instance.hide();\n getToggleButton.focus();\n }\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE$3, Dropdown.dataApiKeydownHandler);\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler);\nEventHandler.on(document, EVENT_CLICK_DATA_API$3, Dropdown.clearMenus);\nEventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus);\nEventHandler.on(document, EVENT_CLICK_DATA_API$3, SELECTOR_DATA_TOGGLE$3, function (event) {\n event.preventDefault();\n Dropdown.getOrCreateInstance(this).toggle();\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Dropdown);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/backdrop.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$9 = 'backdrop';\nconst CLASS_NAME_FADE$4 = 'fade';\nconst CLASS_NAME_SHOW$5 = 'show';\nconst EVENT_MOUSEDOWN = `mousedown.bs.${NAME$9}`;\nconst Default$8 = {\n className: 'modal-backdrop',\n clickCallback: null,\n isAnimated: false,\n isVisible: true,\n // if false, we use the backdrop helper without adding any element to the dom\n rootElement: 'body' // give the choice to place backdrop under different elements\n};\n\nconst DefaultType$8 = {\n className: 'string',\n clickCallback: '(function|null)',\n isAnimated: 'boolean',\n isVisible: 'boolean',\n rootElement: '(element|string)'\n};\n\n/**\n * Class definition\n */\n\nclass Backdrop extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n this._isAppended = false;\n this._element = null;\n }\n\n // Getters\n static get Default() {\n return Default$8;\n }\n static get DefaultType() {\n return DefaultType$8;\n }\n static get NAME() {\n return NAME$9;\n }\n\n // Public\n show(callback) {\n if (!this._config.isVisible) {\n execute(callback);\n return;\n }\n this._append();\n const element = this._getElement();\n if (this._config.isAnimated) {\n reflow(element);\n }\n element.classList.add(CLASS_NAME_SHOW$5);\n this._emulateAnimation(() => {\n execute(callback);\n });\n }\n hide(callback) {\n if (!this._config.isVisible) {\n execute(callback);\n return;\n }\n this._getElement().classList.remove(CLASS_NAME_SHOW$5);\n this._emulateAnimation(() => {\n this.dispose();\n execute(callback);\n });\n }\n dispose() {\n if (!this._isAppended) {\n return;\n }\n EventHandler.off(this._element, EVENT_MOUSEDOWN);\n this._element.remove();\n this._isAppended = false;\n }\n\n // Private\n _getElement() {\n if (!this._element) {\n const backdrop = document.createElement('div');\n backdrop.className = this._config.className;\n if (this._config.isAnimated) {\n backdrop.classList.add(CLASS_NAME_FADE$4);\n }\n this._element = backdrop;\n }\n return this._element;\n }\n _configAfterMerge(config) {\n // use getElement() with the default \"body\" to get a fresh Element on each instantiation\n config.rootElement = getElement(config.rootElement);\n return config;\n }\n _append() {\n if (this._isAppended) {\n return;\n }\n const element = this._getElement();\n this._config.rootElement.append(element);\n EventHandler.on(element, EVENT_MOUSEDOWN, () => {\n execute(this._config.clickCallback);\n });\n this._isAppended = true;\n }\n _emulateAnimation(callback) {\n executeAfterTransition(callback, this._getElement(), this._config.isAnimated);\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/focustrap.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$8 = 'focustrap';\nconst DATA_KEY$5 = 'bs.focustrap';\nconst EVENT_KEY$5 = `.${DATA_KEY$5}`;\nconst EVENT_FOCUSIN$2 = `focusin${EVENT_KEY$5}`;\nconst EVENT_KEYDOWN_TAB = `keydown.tab${EVENT_KEY$5}`;\nconst TAB_KEY = 'Tab';\nconst TAB_NAV_FORWARD = 'forward';\nconst TAB_NAV_BACKWARD = 'backward';\nconst Default$7 = {\n autofocus: true,\n trapElement: null // The element to trap focus inside of\n};\n\nconst DefaultType$7 = {\n autofocus: 'boolean',\n trapElement: 'element'\n};\n\n/**\n * Class definition\n */\n\nclass FocusTrap extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n this._isActive = false;\n this._lastTabNavDirection = null;\n }\n\n // Getters\n static get Default() {\n return Default$7;\n }\n static get DefaultType() {\n return DefaultType$7;\n }\n static get NAME() {\n return NAME$8;\n }\n\n // Public\n activate() {\n if (this._isActive) {\n return;\n }\n if (this._config.autofocus) {\n this._config.trapElement.focus();\n }\n EventHandler.off(document, EVENT_KEY$5); // guard against infinite focus loop\n EventHandler.on(document, EVENT_FOCUSIN$2, event => this._handleFocusin(event));\n EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event));\n this._isActive = true;\n }\n deactivate() {\n if (!this._isActive) {\n return;\n }\n this._isActive = false;\n EventHandler.off(document, EVENT_KEY$5);\n }\n\n // Private\n _handleFocusin(event) {\n const {\n trapElement\n } = this._config;\n if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) {\n return;\n }\n const elements = SelectorEngine.focusableChildren(trapElement);\n if (elements.length === 0) {\n trapElement.focus();\n } else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) {\n elements[elements.length - 1].focus();\n } else {\n elements[0].focus();\n }\n }\n _handleKeydown(event) {\n if (event.key !== TAB_KEY) {\n return;\n }\n this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/scrollBar.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top';\nconst SELECTOR_STICKY_CONTENT = '.sticky-top';\nconst PROPERTY_PADDING = 'padding-right';\nconst PROPERTY_MARGIN = 'margin-right';\n\n/**\n * Class definition\n */\n\nclass ScrollBarHelper {\n constructor() {\n this._element = document.body;\n }\n\n // Public\n getWidth() {\n // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes\n const documentWidth = document.documentElement.clientWidth;\n return Math.abs(window.innerWidth - documentWidth);\n }\n hide() {\n const width = this.getWidth();\n this._disableOverFlow();\n // give padding to element to balance the hidden scrollbar width\n this._setElementAttributes(this._element, PROPERTY_PADDING, calculatedValue => calculatedValue + width);\n // trick: We adjust positive paddingRight and negative marginRight to sticky-top elements to keep showing fullwidth\n this._setElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING, calculatedValue => calculatedValue + width);\n this._setElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN, calculatedValue => calculatedValue - width);\n }\n reset() {\n this._resetElementAttributes(this._element, 'overflow');\n this._resetElementAttributes(this._element, PROPERTY_PADDING);\n this._resetElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING);\n this._resetElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN);\n }\n isOverflowing() {\n return this.getWidth() > 0;\n }\n\n // Private\n _disableOverFlow() {\n this._saveInitialAttribute(this._element, 'overflow');\n this._element.style.overflow = 'hidden';\n }\n _setElementAttributes(selector, styleProperty, callback) {\n const scrollbarWidth = this.getWidth();\n const manipulationCallBack = element => {\n if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) {\n return;\n }\n this._saveInitialAttribute(element, styleProperty);\n const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty);\n element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`);\n };\n this._applyManipulationCallback(selector, manipulationCallBack);\n }\n _saveInitialAttribute(element, styleProperty) {\n const actualValue = element.style.getPropertyValue(styleProperty);\n if (actualValue) {\n Manipulator.setDataAttribute(element, styleProperty, actualValue);\n }\n }\n _resetElementAttributes(selector, styleProperty) {\n const manipulationCallBack = element => {\n const value = Manipulator.getDataAttribute(element, styleProperty);\n // We only want to remove the property if the value is `null`; the value can also be zero\n if (value === null) {\n element.style.removeProperty(styleProperty);\n return;\n }\n Manipulator.removeDataAttribute(element, styleProperty);\n element.style.setProperty(styleProperty, value);\n };\n this._applyManipulationCallback(selector, manipulationCallBack);\n }\n _applyManipulationCallback(selector, callBack) {\n if (isElement(selector)) {\n callBack(selector);\n return;\n }\n for (const sel of SelectorEngine.find(selector, this._element)) {\n callBack(sel);\n }\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap modal.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$7 = 'modal';\nconst DATA_KEY$4 = 'bs.modal';\nconst EVENT_KEY$4 = `.${DATA_KEY$4}`;\nconst DATA_API_KEY$2 = '.data-api';\nconst ESCAPE_KEY$1 = 'Escape';\nconst EVENT_HIDE$4 = `hide${EVENT_KEY$4}`;\nconst EVENT_HIDE_PREVENTED$1 = `hidePrevented${EVENT_KEY$4}`;\nconst EVENT_HIDDEN$4 = `hidden${EVENT_KEY$4}`;\nconst EVENT_SHOW$4 = `show${EVENT_KEY$4}`;\nconst EVENT_SHOWN$4 = `shown${EVENT_KEY$4}`;\nconst EVENT_RESIZE$1 = `resize${EVENT_KEY$4}`;\nconst EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY$4}`;\nconst EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY$4}`;\nconst EVENT_KEYDOWN_DISMISS$1 = `keydown.dismiss${EVENT_KEY$4}`;\nconst EVENT_CLICK_DATA_API$2 = `click${EVENT_KEY$4}${DATA_API_KEY$2}`;\nconst CLASS_NAME_OPEN = 'modal-open';\nconst CLASS_NAME_FADE$3 = 'fade';\nconst CLASS_NAME_SHOW$4 = 'show';\nconst CLASS_NAME_STATIC = 'modal-static';\nconst OPEN_SELECTOR$1 = '.modal.show';\nconst SELECTOR_DIALOG = '.modal-dialog';\nconst SELECTOR_MODAL_BODY = '.modal-body';\nconst SELECTOR_DATA_TOGGLE$2 = '[data-bs-toggle=\"modal\"]';\nconst Default$6 = {\n backdrop: true,\n focus: true,\n keyboard: true\n};\nconst DefaultType$6 = {\n backdrop: '(boolean|string)',\n focus: 'boolean',\n keyboard: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Modal extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element);\n this._backdrop = this._initializeBackDrop();\n this._focustrap = this._initializeFocusTrap();\n this._isShown = false;\n this._isTransitioning = false;\n this._scrollBar = new ScrollBarHelper();\n this._addEventListeners();\n }\n\n // Getters\n static get Default() {\n return Default$6;\n }\n static get DefaultType() {\n return DefaultType$6;\n }\n static get NAME() {\n return NAME$7;\n }\n\n // Public\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget);\n }\n show(relatedTarget) {\n if (this._isShown || this._isTransitioning) {\n return;\n }\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$4, {\n relatedTarget\n });\n if (showEvent.defaultPrevented) {\n return;\n }\n this._isShown = true;\n this._isTransitioning = true;\n this._scrollBar.hide();\n document.body.classList.add(CLASS_NAME_OPEN);\n this._adjustDialog();\n this._backdrop.show(() => this._showElement(relatedTarget));\n }\n hide() {\n if (!this._isShown || this._isTransitioning) {\n return;\n }\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$4);\n if (hideEvent.defaultPrevented) {\n return;\n }\n this._isShown = false;\n this._isTransitioning = true;\n this._focustrap.deactivate();\n this._element.classList.remove(CLASS_NAME_SHOW$4);\n this._queueCallback(() => this._hideModal(), this._element, this._isAnimated());\n }\n dispose() {\n EventHandler.off(window, EVENT_KEY$4);\n EventHandler.off(this._dialog, EVENT_KEY$4);\n this._backdrop.dispose();\n this._focustrap.deactivate();\n super.dispose();\n }\n handleUpdate() {\n this._adjustDialog();\n }\n\n // Private\n _initializeBackDrop() {\n return new Backdrop({\n isVisible: Boolean(this._config.backdrop),\n // 'static' option will be translated to true, and booleans will keep their value,\n isAnimated: this._isAnimated()\n });\n }\n _initializeFocusTrap() {\n return new FocusTrap({\n trapElement: this._element\n });\n }\n _showElement(relatedTarget) {\n // try to append dynamic modal\n if (!document.body.contains(this._element)) {\n document.body.append(this._element);\n }\n this._element.style.display = 'block';\n this._element.removeAttribute('aria-hidden');\n this._element.setAttribute('aria-modal', true);\n this._element.setAttribute('role', 'dialog');\n this._element.scrollTop = 0;\n const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog);\n if (modalBody) {\n modalBody.scrollTop = 0;\n }\n reflow(this._element);\n this._element.classList.add(CLASS_NAME_SHOW$4);\n const transitionComplete = () => {\n if (this._config.focus) {\n this._focustrap.activate();\n }\n this._isTransitioning = false;\n EventHandler.trigger(this._element, EVENT_SHOWN$4, {\n relatedTarget\n });\n };\n this._queueCallback(transitionComplete, this._dialog, this._isAnimated());\n }\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS$1, event => {\n if (event.key !== ESCAPE_KEY$1) {\n return;\n }\n if (this._config.keyboard) {\n this.hide();\n return;\n }\n this._triggerBackdropTransition();\n });\n EventHandler.on(window, EVENT_RESIZE$1, () => {\n if (this._isShown && !this._isTransitioning) {\n this._adjustDialog();\n }\n });\n EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => {\n // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks\n EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => {\n if (this._element !== event.target || this._element !== event2.target) {\n return;\n }\n if (this._config.backdrop === 'static') {\n this._triggerBackdropTransition();\n return;\n }\n if (this._config.backdrop) {\n this.hide();\n }\n });\n });\n }\n _hideModal() {\n this._element.style.display = 'none';\n this._element.setAttribute('aria-hidden', true);\n this._element.removeAttribute('aria-modal');\n this._element.removeAttribute('role');\n this._isTransitioning = false;\n this._backdrop.hide(() => {\n document.body.classList.remove(CLASS_NAME_OPEN);\n this._resetAdjustments();\n this._scrollBar.reset();\n EventHandler.trigger(this._element, EVENT_HIDDEN$4);\n });\n }\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_FADE$3);\n }\n _triggerBackdropTransition() {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED$1);\n if (hideEvent.defaultPrevented) {\n return;\n }\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;\n const initialOverflowY = this._element.style.overflowY;\n // return if the following background transition hasn't yet completed\n if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) {\n return;\n }\n if (!isModalOverflowing) {\n this._element.style.overflowY = 'hidden';\n }\n this._element.classList.add(CLASS_NAME_STATIC);\n this._queueCallback(() => {\n this._element.classList.remove(CLASS_NAME_STATIC);\n this._queueCallback(() => {\n this._element.style.overflowY = initialOverflowY;\n }, this._dialog);\n }, this._dialog);\n this._element.focus();\n }\n\n /**\n * The following methods are used to handle overflowing modals\n */\n\n _adjustDialog() {\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;\n const scrollbarWidth = this._scrollBar.getWidth();\n const isBodyOverflowing = scrollbarWidth > 0;\n if (isBodyOverflowing && !isModalOverflowing) {\n const property = isRTL() ? 'paddingLeft' : 'paddingRight';\n this._element.style[property] = `${scrollbarWidth}px`;\n }\n if (!isBodyOverflowing && isModalOverflowing) {\n const property = isRTL() ? 'paddingRight' : 'paddingLeft';\n this._element.style[property] = `${scrollbarWidth}px`;\n }\n }\n _resetAdjustments() {\n this._element.style.paddingLeft = '';\n this._element.style.paddingRight = '';\n }\n\n // Static\n static jQueryInterface(config, relatedTarget) {\n return this.each(function () {\n const data = Modal.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config](relatedTarget);\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$2, SELECTOR_DATA_TOGGLE$2, function (event) {\n const target = SelectorEngine.getElementFromSelector(this);\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n EventHandler.one(target, EVENT_SHOW$4, showEvent => {\n if (showEvent.defaultPrevented) {\n // only register focus restorer if modal will actually get shown\n return;\n }\n EventHandler.one(target, EVENT_HIDDEN$4, () => {\n if (isVisible(this)) {\n this.focus();\n }\n });\n });\n\n // avoid conflict when clicking modal toggler while another one is open\n const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR$1);\n if (alreadyOpen) {\n Modal.getInstance(alreadyOpen).hide();\n }\n const data = Modal.getOrCreateInstance(target);\n data.toggle(this);\n});\nenableDismissTrigger(Modal);\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Modal);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap offcanvas.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$6 = 'offcanvas';\nconst DATA_KEY$3 = 'bs.offcanvas';\nconst EVENT_KEY$3 = `.${DATA_KEY$3}`;\nconst DATA_API_KEY$1 = '.data-api';\nconst EVENT_LOAD_DATA_API$2 = `load${EVENT_KEY$3}${DATA_API_KEY$1}`;\nconst ESCAPE_KEY = 'Escape';\nconst CLASS_NAME_SHOW$3 = 'show';\nconst CLASS_NAME_SHOWING$1 = 'showing';\nconst CLASS_NAME_HIDING = 'hiding';\nconst CLASS_NAME_BACKDROP = 'offcanvas-backdrop';\nconst OPEN_SELECTOR = '.offcanvas.show';\nconst EVENT_SHOW$3 = `show${EVENT_KEY$3}`;\nconst EVENT_SHOWN$3 = `shown${EVENT_KEY$3}`;\nconst EVENT_HIDE$3 = `hide${EVENT_KEY$3}`;\nconst EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY$3}`;\nconst EVENT_HIDDEN$3 = `hidden${EVENT_KEY$3}`;\nconst EVENT_RESIZE = `resize${EVENT_KEY$3}`;\nconst EVENT_CLICK_DATA_API$1 = `click${EVENT_KEY$3}${DATA_API_KEY$1}`;\nconst EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY$3}`;\nconst SELECTOR_DATA_TOGGLE$1 = '[data-bs-toggle=\"offcanvas\"]';\nconst Default$5 = {\n backdrop: true,\n keyboard: true,\n scroll: false\n};\nconst DefaultType$5 = {\n backdrop: '(boolean|string)',\n keyboard: 'boolean',\n scroll: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Offcanvas extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._isShown = false;\n this._backdrop = this._initializeBackDrop();\n this._focustrap = this._initializeFocusTrap();\n this._addEventListeners();\n }\n\n // Getters\n static get Default() {\n return Default$5;\n }\n static get DefaultType() {\n return DefaultType$5;\n }\n static get NAME() {\n return NAME$6;\n }\n\n // Public\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget);\n }\n show(relatedTarget) {\n if (this._isShown) {\n return;\n }\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$3, {\n relatedTarget\n });\n if (showEvent.defaultPrevented) {\n return;\n }\n this._isShown = true;\n this._backdrop.show();\n if (!this._config.scroll) {\n new ScrollBarHelper().hide();\n }\n this._element.setAttribute('aria-modal', true);\n this._element.setAttribute('role', 'dialog');\n this._element.classList.add(CLASS_NAME_SHOWING$1);\n const completeCallBack = () => {\n if (!this._config.scroll || this._config.backdrop) {\n this._focustrap.activate();\n }\n this._element.classList.add(CLASS_NAME_SHOW$3);\n this._element.classList.remove(CLASS_NAME_SHOWING$1);\n EventHandler.trigger(this._element, EVENT_SHOWN$3, {\n relatedTarget\n });\n };\n this._queueCallback(completeCallBack, this._element, true);\n }\n hide() {\n if (!this._isShown) {\n return;\n }\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$3);\n if (hideEvent.defaultPrevented) {\n return;\n }\n this._focustrap.deactivate();\n this._element.blur();\n this._isShown = false;\n this._element.classList.add(CLASS_NAME_HIDING);\n this._backdrop.hide();\n const completeCallback = () => {\n this._element.classList.remove(CLASS_NAME_SHOW$3, CLASS_NAME_HIDING);\n this._element.removeAttribute('aria-modal');\n this._element.removeAttribute('role');\n if (!this._config.scroll) {\n new ScrollBarHelper().reset();\n }\n EventHandler.trigger(this._element, EVENT_HIDDEN$3);\n };\n this._queueCallback(completeCallback, this._element, true);\n }\n dispose() {\n this._backdrop.dispose();\n this._focustrap.deactivate();\n super.dispose();\n }\n\n // Private\n _initializeBackDrop() {\n const clickCallback = () => {\n if (this._config.backdrop === 'static') {\n EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);\n return;\n }\n this.hide();\n };\n\n // 'static' option will be translated to true, and booleans will keep their value\n const isVisible = Boolean(this._config.backdrop);\n return new Backdrop({\n className: CLASS_NAME_BACKDROP,\n isVisible,\n isAnimated: true,\n rootElement: this._element.parentNode,\n clickCallback: isVisible ? clickCallback : null\n });\n }\n _initializeFocusTrap() {\n return new FocusTrap({\n trapElement: this._element\n });\n }\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {\n if (event.key !== ESCAPE_KEY) {\n return;\n }\n if (this._config.keyboard) {\n this.hide();\n return;\n }\n EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);\n });\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Offcanvas.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config](this);\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$1, SELECTOR_DATA_TOGGLE$1, function (event) {\n const target = SelectorEngine.getElementFromSelector(this);\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n if (isDisabled(this)) {\n return;\n }\n EventHandler.one(target, EVENT_HIDDEN$3, () => {\n // focus on trigger when it is closed\n if (isVisible(this)) {\n this.focus();\n }\n });\n\n // avoid conflict when clicking a toggler of an offcanvas, while another is open\n const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR);\n if (alreadyOpen && alreadyOpen !== target) {\n Offcanvas.getInstance(alreadyOpen).hide();\n }\n const data = Offcanvas.getOrCreateInstance(target);\n data.toggle(this);\n});\nEventHandler.on(window, EVENT_LOAD_DATA_API$2, () => {\n for (const selector of SelectorEngine.find(OPEN_SELECTOR)) {\n Offcanvas.getOrCreateInstance(selector).show();\n }\n});\nEventHandler.on(window, EVENT_RESIZE, () => {\n for (const element of SelectorEngine.find('[aria-modal][class*=show][class*=offcanvas-]')) {\n if (getComputedStyle(element).position !== 'fixed') {\n Offcanvas.getOrCreateInstance(element).hide();\n }\n }\n});\nenableDismissTrigger(Offcanvas);\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Offcanvas);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/sanitizer.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n// js-docs-start allow-list\nconst ARIA_ATTRIBUTE_PATTERN = /^aria-[\\w-]*$/i;\nconst DefaultAllowlist = {\n // Global attributes allowed on any supplied element below.\n '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],\n a: ['target', 'href', 'title', 'rel'],\n area: [],\n b: [],\n br: [],\n col: [],\n code: [],\n div: [],\n em: [],\n hr: [],\n h1: [],\n h2: [],\n h3: [],\n h4: [],\n h5: [],\n h6: [],\n i: [],\n img: ['src', 'srcset', 'alt', 'title', 'width', 'height'],\n li: [],\n ol: [],\n p: [],\n pre: [],\n s: [],\n small: [],\n span: [],\n sub: [],\n sup: [],\n strong: [],\n u: [],\n ul: []\n};\n// js-docs-end allow-list\n\nconst uriAttributes = new Set(['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href']);\n\n/**\n * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation\n * contexts.\n *\n * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38\n */\n// eslint-disable-next-line unicorn/better-regex\nconst SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i;\nconst allowedAttribute = (attribute, allowedAttributeList) => {\n const attributeName = attribute.nodeName.toLowerCase();\n if (allowedAttributeList.includes(attributeName)) {\n if (uriAttributes.has(attributeName)) {\n return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue));\n }\n return true;\n }\n\n // Check if a regular expression validates the attribute.\n return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp).some(regex => regex.test(attributeName));\n};\nfunction sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) {\n if (!unsafeHtml.length) {\n return unsafeHtml;\n }\n if (sanitizeFunction && typeof sanitizeFunction === 'function') {\n return sanitizeFunction(unsafeHtml);\n }\n const domParser = new window.DOMParser();\n const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html');\n const elements = [].concat(...createdDocument.body.querySelectorAll('*'));\n for (const element of elements) {\n const elementName = element.nodeName.toLowerCase();\n if (!Object.keys(allowList).includes(elementName)) {\n element.remove();\n continue;\n }\n const attributeList = [].concat(...element.attributes);\n const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || []);\n for (const attribute of attributeList) {\n if (!allowedAttribute(attribute, allowedAttributes)) {\n element.removeAttribute(attribute.nodeName);\n }\n }\n }\n return createdDocument.body.innerHTML;\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/template-factory.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$5 = 'TemplateFactory';\nconst Default$4 = {\n allowList: DefaultAllowlist,\n content: {},\n // { selector : text , selector2 : text2 , }\n extraClass: '',\n html: false,\n sanitize: true,\n sanitizeFn: null,\n template: '
'\n};\nconst DefaultType$4 = {\n allowList: 'object',\n content: 'object',\n extraClass: '(string|function)',\n html: 'boolean',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n template: 'string'\n};\nconst DefaultContentType = {\n entry: '(string|element|function|null)',\n selector: '(string|element)'\n};\n\n/**\n * Class definition\n */\n\nclass TemplateFactory extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n }\n\n // Getters\n static get Default() {\n return Default$4;\n }\n static get DefaultType() {\n return DefaultType$4;\n }\n static get NAME() {\n return NAME$5;\n }\n\n // Public\n getContent() {\n return Object.values(this._config.content).map(config => this._resolvePossibleFunction(config)).filter(Boolean);\n }\n hasContent() {\n return this.getContent().length > 0;\n }\n changeContent(content) {\n this._checkContent(content);\n this._config.content = {\n ...this._config.content,\n ...content\n };\n return this;\n }\n toHtml() {\n const templateWrapper = document.createElement('div');\n templateWrapper.innerHTML = this._maybeSanitize(this._config.template);\n for (const [selector, text] of Object.entries(this._config.content)) {\n this._setContent(templateWrapper, text, selector);\n }\n const template = templateWrapper.children[0];\n const extraClass = this._resolvePossibleFunction(this._config.extraClass);\n if (extraClass) {\n template.classList.add(...extraClass.split(' '));\n }\n return template;\n }\n\n // Private\n _typeCheckConfig(config) {\n super._typeCheckConfig(config);\n this._checkContent(config.content);\n }\n _checkContent(arg) {\n for (const [selector, content] of Object.entries(arg)) {\n super._typeCheckConfig({\n selector,\n entry: content\n }, DefaultContentType);\n }\n }\n _setContent(template, content, selector) {\n const templateElement = SelectorEngine.findOne(selector, template);\n if (!templateElement) {\n return;\n }\n content = this._resolvePossibleFunction(content);\n if (!content) {\n templateElement.remove();\n return;\n }\n if (isElement(content)) {\n this._putElementInTemplate(getElement(content), templateElement);\n return;\n }\n if (this._config.html) {\n templateElement.innerHTML = this._maybeSanitize(content);\n return;\n }\n templateElement.textContent = content;\n }\n _maybeSanitize(arg) {\n return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg;\n }\n _resolvePossibleFunction(arg) {\n return execute(arg, [this]);\n }\n _putElementInTemplate(element, templateElement) {\n if (this._config.html) {\n templateElement.innerHTML = '';\n templateElement.append(element);\n return;\n }\n templateElement.textContent = element.textContent;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap tooltip.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$4 = 'tooltip';\nconst DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']);\nconst CLASS_NAME_FADE$2 = 'fade';\nconst CLASS_NAME_MODAL = 'modal';\nconst CLASS_NAME_SHOW$2 = 'show';\nconst SELECTOR_TOOLTIP_INNER = '.tooltip-inner';\nconst SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`;\nconst EVENT_MODAL_HIDE = 'hide.bs.modal';\nconst TRIGGER_HOVER = 'hover';\nconst TRIGGER_FOCUS = 'focus';\nconst TRIGGER_CLICK = 'click';\nconst TRIGGER_MANUAL = 'manual';\nconst EVENT_HIDE$2 = 'hide';\nconst EVENT_HIDDEN$2 = 'hidden';\nconst EVENT_SHOW$2 = 'show';\nconst EVENT_SHOWN$2 = 'shown';\nconst EVENT_INSERTED = 'inserted';\nconst EVENT_CLICK$1 = 'click';\nconst EVENT_FOCUSIN$1 = 'focusin';\nconst EVENT_FOCUSOUT$1 = 'focusout';\nconst EVENT_MOUSEENTER = 'mouseenter';\nconst EVENT_MOUSELEAVE = 'mouseleave';\nconst AttachmentMap = {\n AUTO: 'auto',\n TOP: 'top',\n RIGHT: isRTL() ? 'left' : 'right',\n BOTTOM: 'bottom',\n LEFT: isRTL() ? 'right' : 'left'\n};\nconst Default$3 = {\n allowList: DefaultAllowlist,\n animation: true,\n boundary: 'clippingParents',\n container: false,\n customClass: '',\n delay: 0,\n fallbackPlacements: ['top', 'right', 'bottom', 'left'],\n html: false,\n offset: [0, 6],\n placement: 'top',\n popperConfig: null,\n sanitize: true,\n sanitizeFn: null,\n selector: false,\n template: '
' + '
' + '
' + '
',\n title: '',\n trigger: 'hover focus'\n};\nconst DefaultType$3 = {\n allowList: 'object',\n animation: 'boolean',\n boundary: '(string|element)',\n container: '(string|element|boolean)',\n customClass: '(string|function)',\n delay: '(number|object)',\n fallbackPlacements: 'array',\n html: 'boolean',\n offset: '(array|string|function)',\n placement: '(string|function)',\n popperConfig: '(null|object|function)',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n selector: '(string|boolean)',\n template: 'string',\n title: '(string|element|function)',\n trigger: 'string'\n};\n\n/**\n * Class definition\n */\n\nclass Tooltip extends BaseComponent {\n constructor(element, config) {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s tooltips require Popper (https://popper.js.org)');\n }\n super(element, config);\n\n // Private\n this._isEnabled = true;\n this._timeout = 0;\n this._isHovered = null;\n this._activeTrigger = {};\n this._popper = null;\n this._templateFactory = null;\n this._newContent = null;\n\n // Protected\n this.tip = null;\n this._setListeners();\n if (!this._config.selector) {\n this._fixTitle();\n }\n }\n\n // Getters\n static get Default() {\n return Default$3;\n }\n static get DefaultType() {\n return DefaultType$3;\n }\n static get NAME() {\n return NAME$4;\n }\n\n // Public\n enable() {\n this._isEnabled = true;\n }\n disable() {\n this._isEnabled = false;\n }\n toggleEnabled() {\n this._isEnabled = !this._isEnabled;\n }\n toggle() {\n if (!this._isEnabled) {\n return;\n }\n this._activeTrigger.click = !this._activeTrigger.click;\n if (this._isShown()) {\n this._leave();\n return;\n }\n this._enter();\n }\n dispose() {\n clearTimeout(this._timeout);\n EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);\n if (this._element.getAttribute('data-bs-original-title')) {\n this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title'));\n }\n this._disposePopper();\n super.dispose();\n }\n show() {\n if (this._element.style.display === 'none') {\n throw new Error('Please use show on visible elements');\n }\n if (!(this._isWithContent() && this._isEnabled)) {\n return;\n }\n const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW$2));\n const shadowRoot = findShadowRoot(this._element);\n const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element);\n if (showEvent.defaultPrevented || !isInTheDom) {\n return;\n }\n\n // TODO: v6 remove this or make it optional\n this._disposePopper();\n const tip = this._getTipElement();\n this._element.setAttribute('aria-describedby', tip.getAttribute('id'));\n const {\n container\n } = this._config;\n if (!this._element.ownerDocument.documentElement.contains(this.tip)) {\n container.append(tip);\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED));\n }\n this._popper = this._createPopper(tip);\n tip.classList.add(CLASS_NAME_SHOW$2);\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.on(element, 'mouseover', noop);\n }\n }\n const complete = () => {\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN$2));\n if (this._isHovered === false) {\n this._leave();\n }\n this._isHovered = false;\n };\n this._queueCallback(complete, this.tip, this._isAnimated());\n }\n hide() {\n if (!this._isShown()) {\n return;\n }\n const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE$2));\n if (hideEvent.defaultPrevented) {\n return;\n }\n const tip = this._getTipElement();\n tip.classList.remove(CLASS_NAME_SHOW$2);\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.off(element, 'mouseover', noop);\n }\n }\n this._activeTrigger[TRIGGER_CLICK] = false;\n this._activeTrigger[TRIGGER_FOCUS] = false;\n this._activeTrigger[TRIGGER_HOVER] = false;\n this._isHovered = null; // it is a trick to support manual triggering\n\n const complete = () => {\n if (this._isWithActiveTrigger()) {\n return;\n }\n if (!this._isHovered) {\n this._disposePopper();\n }\n this._element.removeAttribute('aria-describedby');\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN$2));\n };\n this._queueCallback(complete, this.tip, this._isAnimated());\n }\n update() {\n if (this._popper) {\n this._popper.update();\n }\n }\n\n // Protected\n _isWithContent() {\n return Boolean(this._getTitle());\n }\n _getTipElement() {\n if (!this.tip) {\n this.tip = this._createTipElement(this._newContent || this._getContentForTemplate());\n }\n return this.tip;\n }\n _createTipElement(content) {\n const tip = this._getTemplateFactory(content).toHtml();\n\n // TODO: remove this check in v6\n if (!tip) {\n return null;\n }\n tip.classList.remove(CLASS_NAME_FADE$2, CLASS_NAME_SHOW$2);\n // TODO: v6 the following can be achieved with CSS only\n tip.classList.add(`bs-${this.constructor.NAME}-auto`);\n const tipId = getUID(this.constructor.NAME).toString();\n tip.setAttribute('id', tipId);\n if (this._isAnimated()) {\n tip.classList.add(CLASS_NAME_FADE$2);\n }\n return tip;\n }\n setContent(content) {\n this._newContent = content;\n if (this._isShown()) {\n this._disposePopper();\n this.show();\n }\n }\n _getTemplateFactory(content) {\n if (this._templateFactory) {\n this._templateFactory.changeContent(content);\n } else {\n this._templateFactory = new TemplateFactory({\n ...this._config,\n // the `content` var has to be after `this._config`\n // to override config.content in case of popover\n content,\n extraClass: this._resolvePossibleFunction(this._config.customClass)\n });\n }\n return this._templateFactory;\n }\n _getContentForTemplate() {\n return {\n [SELECTOR_TOOLTIP_INNER]: this._getTitle()\n };\n }\n _getTitle() {\n return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title');\n }\n\n // Private\n _initializeOnDelegatedTarget(event) {\n return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig());\n }\n _isAnimated() {\n return this._config.animation || this.tip && this.tip.classList.contains(CLASS_NAME_FADE$2);\n }\n _isShown() {\n return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW$2);\n }\n _createPopper(tip) {\n const placement = execute(this._config.placement, [this, tip, this._element]);\n const attachment = AttachmentMap[placement.toUpperCase()];\n return Popper.createPopper(this._element, tip, this._getPopperConfig(attachment));\n }\n _getOffset() {\n const {\n offset\n } = this._config;\n if (typeof offset === 'string') {\n return offset.split(',').map(value => Number.parseInt(value, 10));\n }\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element);\n }\n return offset;\n }\n _resolvePossibleFunction(arg) {\n return execute(arg, [this._element]);\n }\n _getPopperConfig(attachment) {\n const defaultBsPopperConfig = {\n placement: attachment,\n modifiers: [{\n name: 'flip',\n options: {\n fallbackPlacements: this._config.fallbackPlacements\n }\n }, {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n }, {\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n }, {\n name: 'arrow',\n options: {\n element: `.${this.constructor.NAME}-arrow`\n }\n }, {\n name: 'preSetPlacement',\n enabled: true,\n phase: 'beforeMain',\n fn: data => {\n // Pre-set Popper's placement attribute in order to read the arrow sizes properly.\n // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement\n this._getTipElement().setAttribute('data-popper-placement', data.state.placement);\n }\n }]\n };\n return {\n ...defaultBsPopperConfig,\n ...execute(this._config.popperConfig, [defaultBsPopperConfig])\n };\n }\n _setListeners() {\n const triggers = this._config.trigger.split(' ');\n for (const trigger of triggers) {\n if (trigger === 'click') {\n EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK$1), this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n context.toggle();\n });\n } else if (trigger !== TRIGGER_MANUAL) {\n const eventIn = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSEENTER) : this.constructor.eventName(EVENT_FOCUSIN$1);\n const eventOut = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSELEAVE) : this.constructor.eventName(EVENT_FOCUSOUT$1);\n EventHandler.on(this._element, eventIn, this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true;\n context._enter();\n });\n EventHandler.on(this._element, eventOut, this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = context._element.contains(event.relatedTarget);\n context._leave();\n });\n }\n }\n this._hideModalHandler = () => {\n if (this._element) {\n this.hide();\n }\n };\n EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);\n }\n _fixTitle() {\n const title = this._element.getAttribute('title');\n if (!title) {\n return;\n }\n if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) {\n this._element.setAttribute('aria-label', title);\n }\n this._element.setAttribute('data-bs-original-title', title); // DO NOT USE IT. Is only for backwards compatibility\n this._element.removeAttribute('title');\n }\n _enter() {\n if (this._isShown() || this._isHovered) {\n this._isHovered = true;\n return;\n }\n this._isHovered = true;\n this._setTimeout(() => {\n if (this._isHovered) {\n this.show();\n }\n }, this._config.delay.show);\n }\n _leave() {\n if (this._isWithActiveTrigger()) {\n return;\n }\n this._isHovered = false;\n this._setTimeout(() => {\n if (!this._isHovered) {\n this.hide();\n }\n }, this._config.delay.hide);\n }\n _setTimeout(handler, timeout) {\n clearTimeout(this._timeout);\n this._timeout = setTimeout(handler, timeout);\n }\n _isWithActiveTrigger() {\n return Object.values(this._activeTrigger).includes(true);\n }\n _getConfig(config) {\n const dataAttributes = Manipulator.getDataAttributes(this._element);\n for (const dataAttribute of Object.keys(dataAttributes)) {\n if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) {\n delete dataAttributes[dataAttribute];\n }\n }\n config = {\n ...dataAttributes,\n ...(typeof config === 'object' && config ? config : {})\n };\n config = this._mergeConfigObj(config);\n config = this._configAfterMerge(config);\n this._typeCheckConfig(config);\n return config;\n }\n _configAfterMerge(config) {\n config.container = config.container === false ? document.body : getElement(config.container);\n if (typeof config.delay === 'number') {\n config.delay = {\n show: config.delay,\n hide: config.delay\n };\n }\n if (typeof config.title === 'number') {\n config.title = config.title.toString();\n }\n if (typeof config.content === 'number') {\n config.content = config.content.toString();\n }\n return config;\n }\n _getDelegateConfig() {\n const config = {};\n for (const [key, value] of Object.entries(this._config)) {\n if (this.constructor.Default[key] !== value) {\n config[key] = value;\n }\n }\n config.selector = false;\n config.trigger = 'manual';\n\n // In the future can be replaced with:\n // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]])\n // `Object.fromEntries(keysWithDifferentValues)`\n return config;\n }\n _disposePopper() {\n if (this._popper) {\n this._popper.destroy();\n this._popper = null;\n }\n if (this.tip) {\n this.tip.remove();\n this.tip = null;\n }\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Tooltip.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n });\n }\n}\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Tooltip);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap popover.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$3 = 'popover';\nconst SELECTOR_TITLE = '.popover-header';\nconst SELECTOR_CONTENT = '.popover-body';\nconst Default$2 = {\n ...Tooltip.Default,\n content: '',\n offset: [0, 8],\n placement: 'right',\n template: '
' + '
' + '

' + '
' + '
',\n trigger: 'click'\n};\nconst DefaultType$2 = {\n ...Tooltip.DefaultType,\n content: '(null|string|element|function)'\n};\n\n/**\n * Class definition\n */\n\nclass Popover extends Tooltip {\n // Getters\n static get Default() {\n return Default$2;\n }\n static get DefaultType() {\n return DefaultType$2;\n }\n static get NAME() {\n return NAME$3;\n }\n\n // Overrides\n _isWithContent() {\n return this._getTitle() || this._getContent();\n }\n\n // Private\n _getContentForTemplate() {\n return {\n [SELECTOR_TITLE]: this._getTitle(),\n [SELECTOR_CONTENT]: this._getContent()\n };\n }\n _getContent() {\n return this._resolvePossibleFunction(this._config.content);\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Popover.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n });\n }\n}\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Popover);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap scrollspy.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$2 = 'scrollspy';\nconst DATA_KEY$2 = 'bs.scrollspy';\nconst EVENT_KEY$2 = `.${DATA_KEY$2}`;\nconst DATA_API_KEY = '.data-api';\nconst EVENT_ACTIVATE = `activate${EVENT_KEY$2}`;\nconst EVENT_CLICK = `click${EVENT_KEY$2}`;\nconst EVENT_LOAD_DATA_API$1 = `load${EVENT_KEY$2}${DATA_API_KEY}`;\nconst CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item';\nconst CLASS_NAME_ACTIVE$1 = 'active';\nconst SELECTOR_DATA_SPY = '[data-bs-spy=\"scroll\"]';\nconst SELECTOR_TARGET_LINKS = '[href]';\nconst SELECTOR_NAV_LIST_GROUP = '.nav, .list-group';\nconst SELECTOR_NAV_LINKS = '.nav-link';\nconst SELECTOR_NAV_ITEMS = '.nav-item';\nconst SELECTOR_LIST_ITEMS = '.list-group-item';\nconst SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`;\nconst SELECTOR_DROPDOWN = '.dropdown';\nconst SELECTOR_DROPDOWN_TOGGLE$1 = '.dropdown-toggle';\nconst Default$1 = {\n offset: null,\n // TODO: v6 @deprecated, keep it for backwards compatibility reasons\n rootMargin: '0px 0px -25%',\n smoothScroll: false,\n target: null,\n threshold: [0.1, 0.5, 1]\n};\nconst DefaultType$1 = {\n offset: '(number|null)',\n // TODO v6 @deprecated, keep it for backwards compatibility reasons\n rootMargin: 'string',\n smoothScroll: 'boolean',\n target: 'element',\n threshold: 'array'\n};\n\n/**\n * Class definition\n */\n\nclass ScrollSpy extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n\n // this._element is the observablesContainer and config.target the menu links wrapper\n this._targetLinks = new Map();\n this._observableSections = new Map();\n this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element;\n this._activeTarget = null;\n this._observer = null;\n this._previousScrollData = {\n visibleEntryTop: 0,\n parentScrollTop: 0\n };\n this.refresh(); // initialize\n }\n\n // Getters\n static get Default() {\n return Default$1;\n }\n static get DefaultType() {\n return DefaultType$1;\n }\n static get NAME() {\n return NAME$2;\n }\n\n // Public\n refresh() {\n this._initializeTargetsAndObservables();\n this._maybeEnableSmoothScroll();\n if (this._observer) {\n this._observer.disconnect();\n } else {\n this._observer = this._getNewObserver();\n }\n for (const section of this._observableSections.values()) {\n this._observer.observe(section);\n }\n }\n dispose() {\n this._observer.disconnect();\n super.dispose();\n }\n\n // Private\n _configAfterMerge(config) {\n // TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} case\n config.target = getElement(config.target) || document.body;\n\n // TODO: v6 Only for backwards compatibility reasons. Use rootMargin only\n config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin;\n if (typeof config.threshold === 'string') {\n config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value));\n }\n return config;\n }\n _maybeEnableSmoothScroll() {\n if (!this._config.smoothScroll) {\n return;\n }\n\n // unregister any previous listeners\n EventHandler.off(this._config.target, EVENT_CLICK);\n EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => {\n const observableSection = this._observableSections.get(event.target.hash);\n if (observableSection) {\n event.preventDefault();\n const root = this._rootElement || window;\n const height = observableSection.offsetTop - this._element.offsetTop;\n if (root.scrollTo) {\n root.scrollTo({\n top: height,\n behavior: 'smooth'\n });\n return;\n }\n\n // Chrome 60 doesn't support `scrollTo`\n root.scrollTop = height;\n }\n });\n }\n _getNewObserver() {\n const options = {\n root: this._rootElement,\n threshold: this._config.threshold,\n rootMargin: this._config.rootMargin\n };\n return new IntersectionObserver(entries => this._observerCallback(entries), options);\n }\n\n // The logic of selection\n _observerCallback(entries) {\n const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`);\n const activate = entry => {\n this._previousScrollData.visibleEntryTop = entry.target.offsetTop;\n this._process(targetElement(entry));\n };\n const parentScrollTop = (this._rootElement || document.documentElement).scrollTop;\n const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop;\n this._previousScrollData.parentScrollTop = parentScrollTop;\n for (const entry of entries) {\n if (!entry.isIntersecting) {\n this._activeTarget = null;\n this._clearActiveClass(targetElement(entry));\n continue;\n }\n const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop;\n // if we are scrolling down, pick the bigger offsetTop\n if (userScrollsDown && entryIsLowerThanPrevious) {\n activate(entry);\n // if parent isn't scrolled, let's keep the first visible item, breaking the iteration\n if (!parentScrollTop) {\n return;\n }\n continue;\n }\n\n // if we are scrolling up, pick the smallest offsetTop\n if (!userScrollsDown && !entryIsLowerThanPrevious) {\n activate(entry);\n }\n }\n }\n _initializeTargetsAndObservables() {\n this._targetLinks = new Map();\n this._observableSections = new Map();\n const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target);\n for (const anchor of targetLinks) {\n // ensure that the anchor has an id and is not disabled\n if (!anchor.hash || isDisabled(anchor)) {\n continue;\n }\n const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element);\n\n // ensure that the observableSection exists & is visible\n if (isVisible(observableSection)) {\n this._targetLinks.set(decodeURI(anchor.hash), anchor);\n this._observableSections.set(anchor.hash, observableSection);\n }\n }\n }\n _process(target) {\n if (this._activeTarget === target) {\n return;\n }\n this._clearActiveClass(this._config.target);\n this._activeTarget = target;\n target.classList.add(CLASS_NAME_ACTIVE$1);\n this._activateParents(target);\n EventHandler.trigger(this._element, EVENT_ACTIVATE, {\n relatedTarget: target\n });\n }\n _activateParents(target) {\n // Activate dropdown parents\n if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {\n SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE$1, target.closest(SELECTOR_DROPDOWN)).classList.add(CLASS_NAME_ACTIVE$1);\n return;\n }\n for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) {\n // Set triggered links parents as active\n // With both