diff --git a/.github/workflows/DocPreviewCleanup.yml b/.github/workflows/DocPreviewCleanup.yml new file mode 100644 index 00000000..a8f05276 --- /dev/null +++ b/.github/workflows/DocPreviewCleanup.yml @@ -0,0 +1,32 @@ +name: Doc Preview Cleanup + +on: + pull_request: + types: [closed] + +jobs: + doc-preview-cleanup: + # Do not run on forks to avoid authorization errors + # Source: https://github.community/t/have-github-action-only-run-on-master-repo-and-not-on-forks/140840/18 + if: github.repository_owner == 'trixi-framework' + runs-on: ubuntu-latest + steps: + - name: Checkout gh-pages branch + uses: actions/checkout@v2 + with: + ref: gh-pages + + - name: Delete preview and history + shell: bash + run: | + git config user.name "Documenter.jl" + git config user.email "documenter@juliadocs.github.io" + git rm -rf --ignore-unmatch "previews/PR$PRNUM" + git commit -m "delete preview" --allow-empty + git branch gh-pages-new $(echo "delete history" | git commit-tree HEAD^{tree}) + env: + PRNUM: ${{ github.event.number }} + + - name: Push changes + run: | + git push --force origin gh-pages-new:gh-pages diff --git a/.gitignore b/.gitignore index 7f07f820..ad972b16 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,12 @@ **/Manifest.toml out*/ docs/build +docs/src/authors.md public/ coverage/ coverage_report/ **/*.jl.*.cov .vscode/ +examples/*.tec +examples/*.mesh +*.png diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 00000000..5ed020ed --- /dev/null +++ b/AUTHORS.md @@ -0,0 +1,24 @@ +# Authors + +HOHQMesh.jl is maintained by the +[Trixi authors](https://github.com/trixi-framework/Trixi.jl/blob/main/AUTHORS.md). +Its development is coordinated by the *principal developers* who are its main +contributors and can be contacted in case of questions about HOHQMesh. In addition, +there are *contributors* who have provided substantial additions or modifications. +The [*HOHQMesh*](https://github.com/trixi-framework/HOHQMesh) mesh generator itself +is developed by David A. Kopriva. + +## Principal Developers +* [David A. Kopriva](https://www.math.fsu.edu/~kopriva/), + Florida State University, USA +* [Andrew Winters](https://liu.se/en/employee/andwi94), + Linköping University, Sweden + +## Contributors +The following people contributed major additions or modifications to HOHQMesh and +are listed in alphabetical order: + +* David Kopriva +* Hendrik Ranocha +* Michael Schlottke-Lakemper +* Andrew Winters diff --git a/LICENSE.md b/LICENSE.md index 29449acb..15e2bdb7 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,21 +1,21 @@ MIT License -Copyright (c) 2021-present Michael Schlottke-Lakemper +Copyright (c) 2021-present David Kopriva, Andrew Winters, and Michael Schlottke-Lakemper -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Project.toml b/Project.toml index fb57de09..092c5a27 100644 --- a/Project.toml +++ b/Project.toml @@ -5,7 +5,9 @@ version = "0.1.2-pre" [deps] HOHQMesh_jll = "1d5cbd98-5122-5a8a-bea1-c186d986ee7f" +Requires = "ae029012-a4dd-5104-9daa-d747884805df" [compat] HOHQMesh_jll = "1.0" +Requires = "1.1.3" julia = "1.6" diff --git a/docs/make.jl b/docs/make.jl index a1594129..facf11a8 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -5,6 +5,13 @@ using HOHQMesh # Define module-wide setups such that the respective modules are available in doctests DocMeta.setdocmeta!(HOHQMesh, :DocTestSetup, :(using HOHQMesh); recursive=true) +# Get Trixi root directory +hohqmesh_root_dir = dirname(@__DIR__) + +# Copy list of authors to not need to synchronize it manually +authors_text = read(joinpath(hohqmesh_root_dir, "AUTHORS.md"), String) +write(joinpath(@__DIR__, "src", "authors.md"), authors_text) + # Make documentation makedocs( # Specify modules for which docstrings should be shown @@ -15,15 +22,33 @@ makedocs( format = Documenter.HTML( # Disable pretty URLs during manual testing prettyurls = get(ENV, "CI", nothing) == "true", - # Explicitly add favicon as asset - # assets = ["assets/favicon.ico"], # Set canonical URL to GitHub pages URL canonical = "https://trixi-framework.github.io/HOHQMesh.jl/stable" ), # Explicitly specify documentation structure pages = [ "Home" => "index.md", + "Interactive mesh generation" => [ + "interactive_overview.md", + "guided-tour.md", + "interactive-api.md", + "CheatSheet.md", + ], + "Tutorials" => [ + "Overview" => joinpath("tutorials", "introduction.md"), + joinpath("tutorials", "straight_outer_boundary.md"), + joinpath("tutorials", "curved_outer_boundary.md"), + joinpath("tutorials", "spline_curves.md"), + joinpath("tutorials", "create_edit_curves.md"), + ], + "Advanced topics & developers" => [ + "Development" => "development.md", + "GitHub & Git" => "github-git.md", + "Testing" => "testing.md", + ], "Reference" => "reference.md", + "Authors" => "authors.md", + "Contributing" => "contributing.md", "License" => "license.md" ], strict = true # to make the GitHub action fail when doctests fail, see https://github.com/neuropsychology/Psycho.jl/issues/34 @@ -32,5 +57,5 @@ makedocs( deploydocs( repo = "github.com/trixi-framework/HOHQMesh.jl", devbranch = "main", - # push_preview = true + push_preview = true ) diff --git a/docs/src/CheatSheet.md b/docs/src/CheatSheet.md new file mode 100644 index 00000000..89767bc9 --- /dev/null +++ b/docs/src/CheatSheet.md @@ -0,0 +1,97 @@ +# Commands Cheat Sheet + +This provides a quick reference for the syntax +of the interactive construction of boundary curves, background grid, etc. +When possible, the commands presented below give +generic versions of the function calls, e.g., for creating a new curve or +adding the curve to a boundary chain. The script +`interactive_outer_boundary_generic.jl` in the `examples` folder +constructs an identical example mesh as shown in the [Guided tour](@ref) +using generic function calls. + +A thorough description of the functions can be found in the [API](@ref) section. + +The general workflow of the interactive mesh functionality within a REPL session is + +1. Create a project +2. Add boundary curves +3. Add a background grid +4. Add manual refinement (if desired) +5. Generate mesh + +## Project + +``` + p = newProject(, ) +``` + +## [Plotting](@id cs-plotting) + +``` + plotProject!(p, options) + updatePlot!(p, options) +``` + +The `options` are any sum of `MODEL`, `GRID`, `REFINEMENTS`, and `MESH`. +## Curves + +``` + c = new(name, startLocation [x,y,z],endLocation [x,y,z]) *Straight Line* + c = new(name,center [x,y,z],radius, startAngle, endAngle) *Circular Arc* + c = new(name, xEqn, yEqn, zEqn) *Parametric equation* + c = new(name, dataFile) *Spline with data from a file* + c = new(name, nKnots, knotsMatrix) *Spline with given knot values* +``` + +Shown here is the use of the function `new`, which is a shortcut to the full functions, e.g. `newCircularArcCurve`, etc. which have the same arguments. + +## [Manual Refinement](@id cs-manual-refinement) + +``` + r = newRefinementCenter(name, center, gridSize, radius) + r = newRefinementLine(name, type, startPoint, endPoint, gridSize, width) +``` + +## Adding to a Project + +``` + add!(p, c) *Add outer boundary curve* + add!(p, c, ) *Add curve to an inner boundary* + add!(p, r) *Add refinement region* + + addBackgroundGrid!(p, [top, left, bottom, right], [nX, nY, nZ]) *No outer boundary* + addBackgroundGrid!(p, [dx, dy, dz]) *If an outer boundary is present* +``` +Shown here is the use of the function `add!`, which is a shortcut to the full functions, e.g. `addOuterBoundaryCurve`, etc. which have the same arguments. + +## Accessing items + +``` + crv = getCurve(p, curveName) *Get a curve in the outer boundary* + crv = getCurve(p, curveName, boundaryName) *Get a curve in an inner boundary* + indx, chain = getChain(p, boundaryName) *Get a complete inner boundary curve* + r = getRefinementRegion(p, name) +``` + +## Removing from Project + +``` + removeOuterboundary!(p) *Entire outer boundary curve* + removeInnerBoundary!(p, innerBoundaryName) *Entire inner boundary curve* + remove!(p, name) *Curve in outer boundary* + remove!(p, name, innerBoundaryName) *Curve in inner boundary* + removeRefinementRegion!(p, name) +``` + +## Editing items + +All items have set/get methods to edit them. Most actions have `undo()` and `redo()`. +To find out what the next undo/redo actions are, use `undoActionName()` and `redoActionName()` +to print them to the screen. + +## Meshing + +``` + generate_mesh(p) + remove_mesh!(p) +``` diff --git a/docs/src/contributing.md b/docs/src/contributing.md new file mode 100644 index 00000000..7f523353 --- /dev/null +++ b/docs/src/contributing.md @@ -0,0 +1,54 @@ +# Contributing + +HOHQMesh.jl is an open-source project and we are very happy to accept contributions +from the community. Please feel free to open issues or submit patches (preferably +as merge requests) any time. For planned larger contributions, it is often +beneficial to get in contact with one of the principal developers first (see +[Authors](@ref)). + +HOHQMesh.jl and its contributions are licensed under the MIT license (see +[License](@ref)). As a contributor, you certify that all your +contributions are in conformance with the *Developer Certificate of Origin +(Version 1.1)*, which is reproduced below. + +## Developer Certificate of Origin (Version 1.1) +The following text was taken from +[https://developercertificate.org](https://developercertificate.org): + + Developer Certificate of Origin + Version 1.1 + + Copyright (C) 2004, 2006 The Linux Foundation and its contributors. + 1 Letterman Drive + Suite D4700 + San Francisco, CA, 94129 + + Everyone is permitted to copy and distribute verbatim copies of this + license document, but changing it is not allowed. + + + Developer's Certificate of Origin 1.1 + + By making a contribution to this project, I certify that: + + (a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + + (b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + + (c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + + (d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. diff --git a/docs/src/development.md b/docs/src/development.md new file mode 100644 index 00000000..4ea9c970 --- /dev/null +++ b/docs/src/development.md @@ -0,0 +1,67 @@ +# Development + +## Text editors +When writing code, the choice of text editor can have a significant impact on +productivity and developer satisfaction. While using the default text editor +of the operating system has its own benefits (specifically the lack of an explicit +installation procure), usually it makes sense to switch to a more +programming-friendly tool. In the following, a few of the many options are +listed and discussed: + +### VS Code +[Visual Studio Code](https://code.visualstudio.com/) is a modern open source +editor with [good support for Julia](https://github.com/julia-vscode/julia-vscode). +While [Juno](#Juno) had some better support in the past, the developers of Juno +and the Julia VS Code plugin are joining forces and concentrating on VS Code +since support of Atom has been suspended. Basically, all comments on [Juno](#Juno) +below also apply to VS Code. + +### Juno +If you are new to programming or do not have a preference for a text editor +yet, [Juno](https://junolab.org) is a good choice for developing Julia code. +It is based on *Atom*, a sophisticated and widely used editor for software +developers, and is enhanced with several Julia-specific features. Furthermore +and especially helpful for novice programmers, it has a MATLAB-like +appearance with easy and interactive access to the current variables, the +help system, and a debugger. + +### Vim or Emacs +Vim and Emacs are both very popular editors that work great with Julia. One +of their advantages is that they are text editors without a GUI and as such +are available for almost any operating system. They also are preinstalled on +virtually all Unix-like systems. However, Vim and Emacs come with their own, +steep learning curve if they have never been used before. Therefore, if in doubt, it +is probably easier to get started with a classic GUI-based text editor (like +Juno). If you decide to use Vim or Emacs, make sure that you install the +corresponding Vim plugin +[julia-vim](https://github.com/JuliaEditorSupport/julia-vim) or Emacs major +mode [julia-emacs](https://github.com/JuliaEditorSupport/julia-emacs). + + + +## Releasing a new version of HOHQMesh + +- Check whether everything is okay, tests pass etc. +- Set the new version number in `Project.toml` according to the Julian version of semver. + Commit and push. +- Comment `@JuliaRegistrator register` on the commit setting the version number. +- `JuliaRegistrator` will create a PR with the new version in the General registry. + Wait for it to be merged. +- Increment the version number in `Project.toml` again with suffix `-pre`. For example, + if you have released version `v0.2.0`, use `v0.2.1-pre` as new version number. + + + +## Preview the documentation + +You can build the documentation of HOHQMesh.jl locally by running +```bash +julia --project=docs -e 'using Pkg; Pkg.instantiate(); include("docs/make.jl")' +``` +from the HOHQMesh.jl main directory. Then, you can look at the html files generated in +`docs/build`. +For PRs triggered from branches inside the HOHQMesh.jl main repository previews of +the new documentation are generated at `https://trixi-framework.github.io/HOHQMesh.jl/previews/PRXXX`, +where `XXX` is the number of the PR. +Note, this does not work for PRs from forks for security reasons (since anyone could otherwise push +arbitrary stuff, including malicious code). \ No newline at end of file diff --git a/docs/src/github-git.md b/docs/src/github-git.md new file mode 100644 index 00000000..d2f761f5 --- /dev/null +++ b/docs/src/github-git.md @@ -0,0 +1,226 @@ +# GitHub & Git + +This page contains information on how to use GitHub and Git when developing +HOHQMesh. + +## Development workflow + +For adding modifications to HOHQMesh, we generally follow these steps: + +### Create an issue (optional) +In many cases it makes sense to start by creating an issue on GitHub. For +example, if the implementation approach for a new feature is not yet clear or if +there should be a discussion about the desired outcome, it is good practice to +first get a consensus on what is the expected result of this modification. A +GitHub issue is *the* place to lead this discussion, as it preserves it in the +project and - together with the actual code changes - allows in the future to revisit +the reasons for a particular choice of implementation or feature. + +### Create a branch and *immediately* create a pull request +All feature development, bug fixes etc. should be developed in a branch and not +directly on `main`. If you do not have write access to the main repository on +GitHub, first create a fork of the HOHQMesh.jl repository and clone the fork to +your machine. Then, create a branch locally by executing `git checkout -b +yourbranch`, push it to the repository, and create a pull request (PR). + +If you have already cloned HOHQMesh.jl from the main repo to your local machine, +you can also work in that clone. You just need to add your fork as additional +remote repository and push your new branch there. +```bash +git remote add myfork git@github.com:YOUR_NAME/HOHQMesh.jl.git +# get latest main from the main repo +git checkout main +git pull +# create a new branch for a cool new feature, bug fix, ... +git checkout -b YOUR_BRANCH_NAME +# do some work and push it to your fork +git push -u myfork +# go to https://github.com/trixi-framework/HOHQMesh.jl/pull +# and create a PR from your new branch +``` + +!!! info "Why using pull requests?" + Immediately creating a PR for your branch has the benefit that all + code discussions can now be held directly next to the corresponding code. Also, + the PR allows to easily compare your branch to the upstream branch + (usually `main`) to see what you have changed. Moreover, tests will run + automatically. + +### Make changes +With a branch and PR in place, you can now write your code and commit +it to your branch. If you request feedback from someone else, make sure to push +your branch to the repository such that the others can easily review your +changes or dive in and change something themselves. + +!!! warning "Avoid committing unwanted files" + When you use `git add .` or similar catch-all versions, make sure you do not + accidentally commit unwanted files (e.g., HOHQMesh output plot and mesh files). + If it happens anyways, you can undo the last commit (also + multiple times) by running `git reset HEAD~` (see also [Undo last + commit](@ref)). However, this strategy only works if you have **not yet + pushed your changes**. If you *did* push your changes, please talk to one of + the principal developers on how to proceed. + +### Keep your branch in sync with `main` +For larger features with longer-living branches, it may make sense to +synchronize your branch with the current `main`, e.g., if there was a bug fix +in `main` that is relevant for you. In this case, perform the following steps to +merge the current `main` to your branch: + + 1. Commit all your local changes to your branch and push it. This allows you to + delete your clone in case you make a mistake and need to abort the merge. + 2. Execute `git fetch` to get the latest changes from the repository. + 3. Make sure you are in the correct branch by checking the output of `git status` + or by running `git checkout yourbranch`. + 4. Merge main using `git merge main`. If there were no conflicts, hooray!, + you are done. Otherwise you need to resolve your merge conflicts and commit + the changes afterwards. A good guide for resolving merge conflicts can be + found + [here](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/resolving-a-merge-conflict-using-the-command-line). + +In general, always use `git merge` and not `git rebase` to get the latest +changes from `main`. It is less error-prone and does not create problems on +branches that are worked on collaboratively. + +### Prepare for review +If you feel like your branch is ready to be merged to `main`, prepare it for +review. That is, you should + + * merge the current `main` to your branch + * run tests if available, but at least ensure that you did not accidentally + change the results for one of the existing example elixirs + * properly comment your code + * delete old/unused code, especially commented lines (unless they contain + helpful code, in which case you should add a comment on why you keep this + around) + * remove debug statements + * add a script `interactive_xxx.jl` that uses your feature (only relevant for new + features within the interactive mesh funtionality) + +After you are confident that your branch is cleaned up properly, commit all +changes and push them to the repository. + +### Get reviewed +Ask one of the principal developers to review your code. This review will be +conducted asynchronously, with the reviewer leaving comments and annotations in the PR. +In some cases it will be necessary to do multiple rounds of reviews, especially if +there are larger changes to be added. Just commit and push your changes to your +branch, and the corresponding pull request will be updated automatically. + +Please note that a review has nothing to do with the lack of experience of the +person developing changes: We try to review all code before it gets added to +`main`, even from the most experienced developers. This is good practice and +helps to keep the error rate low while ensuring the the code is developed in a +consistent fashion. Furthermore, do not take criticism of your code personally - +we just try to keep HOHQMesh as accessible and easy to use for everyone. + +### Merge branch +Once your branch is reviewed and declared ready for merging by the reviewer, +make sure that all the latest changes have been pushed. Then, one of the +developers will merge your PR. If you are one of the developers, you can also go +to the pull request page on GitHub and and click on **Merge pull request**. +Voilá, you are done! Your branch will have been merged to +`main` and the source branch will have been deleted in the GitHub repository +(if you are not working in your own fork). + +### Update your working copy +Once you have merged your branch by accepting the PR on GitHub, you +should clean up your local working copy of the repository by performing the +following steps: + + 1. Update your clone by running `git fetch`. + 2. Check out `main` using `git checkout main`. + 3. Delete merged branch locally with `git branch -d yourbranch`. + 4. Remove local references to deleted remote branch by executing `git remote + prune origin`. + +You can now proceed with your next changes by starting again at the top. + + +## Using Git + +### Resources for learning Git +Here are a few resources for learning do use Git that at least one of us found +helpful in the past (roughly ordered from novice to advanced to expert): + + * [Git Handbook by GitHub](https://guides.github.com/introduction/git-handbook/) + * [Learn Git Branching](https://learngitbranching.js.org/) + +### Tips and tricks +This is an unordered collection of different tips and tricks that can be helpful +while working with Git. As usual, your mileage might vary. + +#### Undo last commit +If you made a mistake in your last commit, e.g., by committing an unwanted file, +you can undo the latest commit by running +```bash +git reset HEAD~ +``` +This only works if you have not yet pushed your branch to the GitHub repository. +In this case, please talk to one of the core developers on how to proceed. +Especially when you accidentally committed a large file (image, or video), please +let us know as fast as possible, since the effort to fix the repository grows +considerably over time. + +#### Remove large file from repository +If a large file was accidentally committed **and pushed** to the HOHQMesh +repository, please talk to one of the principal developers as soon as possible so +that they can fix it. + +!!! danger "Large files" + You should never try to fix this yourself, as it potentially + disrupts/destroys the work of others! + +Based on the instructions found +[here](https://rtyley.github.io/bfg-repo-cleaner/) and +[here](https://docs.github.com/en/github/authenticating-to-github/removing-sensitive-data-from-a-repository), +the following steps need to be taken (as documented for GitLab in issue +[#33](https://github.com/trixi-framework/Trixi.jl/issues/33)): + + 1. Tell everyone to commit and push their changes to the repository. + 2. Fix the branch in which the file was committed by removing it and committing + the removal. This is especially important on `main`. + 3. Perform the following steps to clean up the Git repository: + ```bash + cd /tmp + + # Download bfg-1.13.0.jar from https://rtyley.github.io/bfg-repo-cleaner/ + + # Get fresh clone of repo (so you can throw it away in case there is a problem) + git clone --mirror git@github.com:trixi-framework/HOHQMesh.jl.git + + # Clean up repo of all files larger than 10M + java -jar bfg-1.13.0.jar --strip-blobs-bigger-than 10M HOHQMesh.jl.git + + # Enter repo + cd Trixi.jl.git + + # Clean up reflog and force aggressive garbage collection + git reflog expire --expire=now --all && git gc --prune=now --aggressive + + # Push changes + git push + + # Delete clone + rm -rf HOHQMesh.jl.git + ``` + 4. Tell everyone to clean up their local working copies by performing the + following steps (also do this yourself): + ```bash + # Enter repo + cd HOHQMesh.jl + + # Get current changes + git fetch + + # Check out the fixed branch + git checkout branchname + + # IMPORTANT: Do a rebase instead of a pull! + git rebase + + # Clean reflog and force garbage collection + git reflog expire --expire=now --all && git gc --prune=now --aggressive + ``` + **IMPORTANT**: You need to do a `git rebase` instead of a `git pull` when + updating the fixed branch. diff --git a/docs/src/guided-tour.md b/docs/src/guided-tour.md new file mode 100644 index 00000000..2dc7ceaa --- /dev/null +++ b/docs/src/guided-tour.md @@ -0,0 +1,142 @@ +# Guided tour + +In this brief overview, we highlight two scripts from the `examples` folder +that demonstrate the interactive mesh functionality of HOHQMesh.jl. In depth +explanations of the functionality are provided in the [Tutorials](@ref). + +See the [HOHQMesh documentation](https://trixi-framework.github.io/HOHQMesh/) +for more details about its terminology and capabilities. + +## Mesh from a control file + +A first example script reads in an existing control file from the HOHQMesh examples collection +and makes it into a `Project`. +To run this example, execute +```julia +julia> include(joinpath(HOHQMesh.examples_dir(), "interactive_from_control_file.jl")) +``` +This command will create mesh and plot files in the `out` directory. + +## Build a mesh from scratch + +A second example script `interactive_outer_boundary.jl` makes a new project consisting +of an outer, circular boundary, and an inner boundary in the shape of an ice cream cone. +To run this example, execute +```julia +julia> include(joinpath(HOHQMesh.examples_dir(), "interactive_outer_boundary.jl")) +``` + +For completeness, we provide the example script and walk-through each step in the construction +of the `Project` below. +```julia +using HOHQMesh + +# Create a new project with the name "IceCreamCone", which will also be the +# name of the mesh, plot and stats files, written to output folder `out`. + +p = newProject("IceCreamCone", "out") + +# Outer boundary for this example mesh is a complete circle. Add it into the project. + +circ = newCircularArcCurve("outerCircle", [0.0, -1.0, 0.0], 4.0, 0.0, 360.0, "degrees") +addCurveToOuterBoundary!(p, circ) + +# Inner boundary is three curves. Two straight lines and a circular arc. +# Note the three curve are connected to ensure a counter-clockwise orientation +# as required by HOHQMesh + +# Create the three interior curves. The individual names of each curve in the inner +# chain are used internally by HOHQMesh and are output as the given boundary names in +# the mesh file. + +cone1 = newEndPointsLineCurve("cone1", [0.0, -3.0, 0.0], [1.0, 0.0, 0.0]) +iceCream = newCircularArcCurve("iceCream", [0.0, 0.0, 0.0], 1.0, 0.0, 180.0, "degrees") +cone2 = newEndPointsLineCurve("cone2", [-1.0, 0.0, 0.0], [0.0, -3.0, 0.0]) + +# Assemble the three curve in a closed chain oriented counter-clockwise. The chain +# name `IceCreamCone` is only used internally by HOHQMesh. + +addCurveToInnerBoundary!(p, cone1, "IceCreamCone") +addCurveToInnerBoundary!(p, iceCream, "IceCreamCone") +addCurveToInnerBoundary!(p, cone2, "IceCreamCone") + +# Adjust some `RunParameters` and overwrite the defaults values. In this case, we +# set a new value for the boundary order polynomial representation and adjust the +# output mesh file format to be `sem` + +setPolynomialOrder!(p, 4) +setPlotFileFormat!(p, "sem") + +# A background grid is required for the mesh generation. In this example we lay a +# background grid of Cartesian boxes with size 0.5. + +addBackgroundGrid!(p, [0.5, 0.5, 0.0]) + +# Plot the project model curves and background grid + +if isdefined(Main, :Makie) + plotProject!(p, MODEL+GRID) + @info "Press enter to generate the mesh and update the plot." + readline() + else # Throw an informational message about plotting to the user + @info "To visualize the project (boundary curves, background grid, mesh, etc.), include `GLMakie` and run again." + end + +# Generate the mesh. This produces the mesh and TecPlot files `AllFeatures.mesh` and `AllFeatures.tec` +# and save them to the `out` folder. Also, if there is an active plot in the project `p` it is +# updated with the mesh that was generated. + +generate_mesh(p) + +# After the mesh successfully generates mesh statistics, such as the number of corner nodes, +# the number of elements etc., are printed to the REPL +``` +The first line creates a new project, where the mesh and plot file names will be derived +from the project name, "IceCreamCone" written to the specified folder. + +To develop the model, one adds curves to the outer boundary or to multiple inner boundaries, +if desired. As in HOHQMesh, there are four curve classes currently available: + +- Parametric equations +- Cubic Splines +- Lines defined by their end points +- Circular arcs + +In the example, the outer boundary is a closed circular arc with center at [0.0, 0.0, 0.0] +with radius 4, starting at zero and ending at 360 degrees. It is added to the project with +`addCurveToOuterBoundary!`. You can add any number of curves to the outer boundary. + +Similarly, you create curves and add them to as many inner boundaries that you want to have. +In the example, there is one inner boundary, "IceCreamCone" made up of two straight lines and a half +circular arc. Again, they are defined counter-clockwise. + +For convenience, `newProject` will generate default run parameters used by HOHQMesh, like the plot file format +and the smoother. The parameters can be edited with setter commands. For example, the script +sets the polynomial order (default = 5) and the plot file format (default = "skeleton"). + +One run parameter that must be set manually is the background grid. Since there is an outer +boundary, that determines the extent of the domain to be meshed, so only the mesh size needs +to be specified using +``` +addBackgroundGrid!(proj::Project, bgSize::Array{Float64}) +``` + +The example sets the background mesh size to be 0.1 in the x and y directions. +The z component is ignored. + +The script finishes by generating the quad mesh and plotting the results, as shown below + +![iceCreamCone](https://user-images.githubusercontent.com/25242486/162193980-b80fb92c-2851-4809-af01-be856152514f.png) + +Finally, the script returns the project so that it can be edited further, if desired. + +To save a control file for HOHQMesh, simply invoke +``` +saveProject(proj::Project, outFile::String) +``` +where `outFile` is the name of the control file (traditionally with a .control extension). +`saveProject` is automatically called when a mesh is generated. + +Note, a third example script `interactive_outer_boundary_generic.jl` is identical to that +which was explained above except that the function calls use the generic versions of +functions, e.g., `new` or `add!`. diff --git a/docs/src/index.md b/docs/src/index.md index 4afc0d97..32709ff9 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,15 +1,19 @@ # HOHQMesh.jl -This package is a thin Julia wrapper around the *High Order Hex-Quad Mesher* +This package is a Julia frontend to the Fortran-based *High Order Hex-Quad Mesher* (a.k.a. [**HOHQMesh**](https://github.com/trixi-framework/HOHQMesh)) created and developed by -[David A. Kopriva](https://www.math.fsu.edu/~kopriva/). +[David A. Kopriva](https://www.math.fsu.edu/~kopriva/). It augments HOHQMesh with +[interactive functionality](@ref InteractiveTool) that gives a user the ability to create, visualize, +and generate high-order meshes. +It further allows one to seamlessly integrate meshes generated by HOHQMesh into a Julia-based simulation workflow. +For example, running a simulation on an unstructured quadrilateral mesh +with [Trixi.jl](https://trixi-framework.github.io/Trixi.jl/stable/tutorials/hohqmesh_tutorial/). HOHQMesh.jl is available on Linux, MacOS, and Windows. - ## Installation If you have not yet installed Julia, please [follow the instructions for your operating system](https://julialang.org/downloads/platform/). HOHQMesh.jl works -with Julia v1.6. +with Julia v1.6 and above. HOHQMesh.jl is a registered Julia package. Hence, you can install it by executing the following commands in the Julia REPL: @@ -47,18 +51,50 @@ while the 3D file `Snake.control` produces this mesh: ![snake_400px](https://user-images.githubusercontent.com/3637659/117241963-8ce0b080-ae34-11eb-9b79-d091807d9a23.png) +Examples scripts of interactive mesh generation tools are available in the +[examples/](https://github.com/trixi-framework/HOHQMesh.jl/tree/main/examples) subdirectory. +These example scripts are prefaced with the phrase `interactive_`. +There is a brief summary at the top of each `interactive` example script that describes +the mesh it will create and the features of HOHQMesh it uses. +An example script can be executed from a Julia REPL session with an `include(...)` statement, e.g., +```julia +julia> include(joinpath(HOHQMesh.examples_dir(), "interactive_outer_box_two_circles.jl")) +``` +The resulting output mesh and plot files are saved in the output directory `out` as +designated in the example script. Mesh statistics are printed to the screen. + +The interactive functionality uses [Makie.jl](https://github.com/JuliaPlots/Makie.jl/) +to visualize the boundary curves and mesh from the interactive tool. A Makie backend, such as +[GLMakie](https://github.com/JuliaPlots/GLMakie.jl/), can +be loaded in addition to HOHQMesh +```julia +julia> using Pkg; Pkg.add("GLMakie") +julia> using GLMakie +``` +Now, running the example script produces a figure in addition to the mesh and plot +files that are saved and the output of mesh statistics to the screen. + +![box_two_circles](https://user-images.githubusercontent.com/25242486/174244295-40d31df3-981e-4375-bc3a-af0a43737710.png) + +Further explanation of the interactive functionality can be found [here](@ref InteractiveTool). +Additional examples are available in the [Tutorials](@ref). -## Authors -HOHQMesh.jl was initiated by -[Michael Schlottke-Lakemper](https://www.mi.uni-koeln.de/NumSim/schlottke-lakemper) -(University of Cologne, Germany), who is also the principal developer of HOHQMesh.jl. -The *HOHQMesh* mesh generator itself is developed by -[David A. Kopriva](https://www.math.fsu.edu/~kopriva/). +## [Authors](@id authors-index-md) +HOHQMesh.jl is maintained by the +[Trixi authors](https://github.com/trixi-framework/Trixi.jl/blob/main/AUTHORS.md). +Its principal developers are [Andrew Winters](https://liu.se/en/employee/andwi94) +(Linköping University, Sweden) and [David A. Kopriva](https://www.math.fsu.edu/~kopriva/). +The *HOHQMesh* mesh generator itself is developed by David A. Kopriva. ## License and contributing HOHQMesh.jl is licensed under the MIT license (see [License](@ref)). *HOHQMesh* itself is also available under the MIT license. +Since HOHQMesh is an open-source project, we are very happy to accept contributions +from the community. Please refer to [Contributing](@ref) for more details. +To get in touch with the developers, +[join us on Slack](https://join.slack.com/t/trixi-framework/shared_invite/zt-sgkc6ppw-6OXJqZAD5SPjBYqLd8MU~g) +or [create an issue](https://github.com/trixi-framework/HOHQMesh.jl/issues/new). ## Acknowledgements diff --git a/docs/src/interactive-api.md b/docs/src/interactive-api.md new file mode 100644 index 00000000..103f150d --- /dev/null +++ b/docs/src/interactive-api.md @@ -0,0 +1,482 @@ +# API + +## Project creation and saving + +### New `Project` +``` + [Return:Project] proj = newProject(name::String, folder::String) +``` +The supplied name will be the default name of the mesh and plot files generated by HOHQMesh. The folder is +the directory in which those files will be placed. The empty project will include default `RunParameters` +and a default `SpringSmoother`, both of which can be modified later, if desired. The only thing required to +add is the background grid. + +### Opening an existing project file + +A project can be created from an existing HOHQMesh control file with +``` + [Return:Project] proj = openProject(fileName::String, folder::String) +``` +The supplied `fileName` will be the name of the project and the generated mesh and plot files will be placed +in the supplied `folder`. + +### Saving a `Project` +``` + saveProject(proj::Project) +``` +writes a control file to the folder designated when creating the new project. +It can be read in again with `openProject`. + +## Plotting a `Project` + +``` + plotProject!(proj::Project, options) +``` +The plot options are any combination of `MODEL`, `GRID`, `REFINEMENTS`, and `MESH`. +`MODEL` refers to any inner / outer boundary curves contained in the project. +`GRID` refers to the background grid, which you an view to make sure that it can resolve +the boundary curves in the model. +`REFINEMENTS` will show the placement where user defined manual refinement regions are placed. +`MESH` refers to the actual mesh of quadrilateral elements generated by HOHQMesh. +Before meshing one probably wants to view `MODEL+GRID`, and afterwards, `MODEL+MESH`. + +If the model is modified and you want to re-plot with the new values, invoke +``` + updatePlot!(proj::Project, options) +``` +but generally the plot will be updated automatically as you build the model. + +## Modifying/editing a `Project` + +### Setting the name of a `Project` + +The project name is the name under which the mesh, plot, statistics and control files will be written. +It can be changed at any time with +``` + setName!(proj::Project,name::String) +``` + +### Getting the current name of a `Project` +The project name can be fetched and printed to the screen with +``` + [Return:String] getName(proj::Project) +``` + +## Controlling the mesh generation + +### Editing the `RunParameters` + +The run parameters can be enquired and set with these getter/setter pairs: +``` + [Return:nothing] setPolynomialOrder!(proj::Project, p::Int) + [Return:Int] getPolynomialOrder(proj::Project) + [Return:nothing] setMeshFileFormat!(proj::Project, meshFileFormat::String) + [Return:String] getMeshFileFormat(proj::Project) + [Return:nothing] setPlotFileFormat!(proj::Project, plotFileFormat::String) + [Return:String] getPlotFileFormat(proj::Project) +``` + +HOHQMesh generates mesh files that contain high-order polynomial interpolations for any +curved boundaries in the model. The degree of this polynomial representation is controlled +by the above getter/setter pair. The default polynomial order is set to `5`. + +The available mesh file formats are `"ISM"`, `"ISM-V2"`, or `"ABAQUS"`. See the HOHQMesh documentation, +[Appendix A](https://trixi-framework.github.io/HOHQMesh/appendix/#appendix-a-additions-for-ism-v2) +or [Appendix E](https://trixi-framework.github.io/HOHQMesh/appendix/#appendix-e-abaqus-mesh-file-format), +as well as the Trixi.jl documentation, +[Unstructured quadrilateral mesh](https://trixi-framework.github.io/Trixi.jl/stable/meshes/unstructured_quad_mesh/) +or +[P4est-based mesh](https://trixi-framework.github.io/Trixi.jl/stable/meshes/p4est_mesh/), +for details and discussions on the latter two mesh file formats. + +The plot file (which can be viewed with something like VisIt or ParaView) format is either `"skeleton"` or `"sem"`. +The former is just a low order finite element representation of the mesh. +The latter (which is a much bigger file) includes the interior degrees of freedom. + +### Changing the output file names + +By default, the mesh, plot and stats files will be written with the name and path supplied when +`newProject` is called. They can be changed/enquired with +``` + [Return:nothing] setName!(proj::Project,name::String) + [Return:String] getName(proj::Project) + [Return:nothing] setFolder!(proj::Project,folder::String) + [Return:String] getFolder(proj::Project) +``` + +### Smoothing operations + +A default smoother is created when `newProject` is called, which sets the status to `ON`, type to +`LinearAndCrossbarSpring`, and number of iterations = 25. These are generally good enough for most purposes. +The most likely parameter to change is the number of iterations. Further details on the smoothing strategy +and how it works are available [here](https://trixi-framework.github.io/HOHQMesh/the-control-input/#the-smoother). + +To change the defaults, the smoother parameters can be set/enquired with the functions +``` + [Return:nothing] setSmoothingStatus!(proj::Project, status::String) + [Return:String] getSmoothingStatus(proj::Project) + [Return:nothing] setSmoothingType!(proj::Project, type::String) + [Return:String] getSmoothingType(proj::Project) + [Return:nothing] setSmoothingIterations!(proj::Project, iterations::Int) + [Return:Int] getSmoothingIterations(proj::Project) +``` +The smooth `status` is either "ON" or "OFF". + +To remove the smoother altogether, use +``` + [Return:nothing] removeSpringSmoother!(proj::Project) +``` + +### Adding the background grid + +There are three forms for the background grid definition, one for when there is an outer boundary, +and two for when there is not. One form of background grid **must** to be specified +after a new project has been created. +``` + [Return:nothing] addBackgroundGrid!(proj::Project, + x0::Array{Float64}, + dx::Array{Float64}, + N::Array{Int}) + [Return:nothing] addBackgroundGrid!(proj::Project, + box::Array{Float64}, + N::Array{Int}) + [Return:nothing] addBackgroundGrid!(proj::Project, + bgSize::Array{Float64}) +``` +Use one of the first two if there is no outer boundary present in the model. With the first, a rectangular +outer boundary will be created of extent `[x0[1], x0[1] + N*dx[1]]` by `[x0[2], x0[2] + N*dx[2]]`. +The second lets you set the bounding box = [top, left, bottom, right], and the number of points in each direction. +The arrays `x0`, `dx`, and `N` are all vectors `[ *, *, * ]` giving the x, y, and z components. +When an outer boundary is present use the third variant where one only need specify the desired background grid size +with the vector `bgSize`. + +### Changing the background grid + +The size of an existing background grid in a `Project` can be adjusted with +``` + [Return:nothing] setBackgroundGridSize!(proj::Project, + dx::Float64, + dy::Float64) +``` +If a plot is present it will be updated automatically. + +### Manual refinement regions + +Refinement can be specified either at a point, using the `RefinementCenter`, or along a line, +using a `RefinementLine`. You can have as many of these refinement regions as you want. +They are useful if you know regions of the solution where refinement is needed +(e.g. a wake) or in problematic areas of the geometry (e.g a sharp corner). + +To create a `RefinementCenter`, +``` + [Return:Dict{String,Any}] newRefinementCenter(name::String, + type::String, + x0::Array{Float64}, + h::Float64, + w::Float64) +``` +where `name` labels the refinement region, the `type` is either "smooth" or "sharp", +`x0` = [x, y, z] is the location of the center, `h` is the mesh size, +and `w` is the extent of the refinement region. The z component must be zero. + +Similarly, one can create a `RefinementLine`, +``` + [Return:Dict{String,Any}] newRefinementLine(name::String, + type::String, + x0::Array{Float64}, x1::Array{Float64}, + h::Float64, + w::Float64) +``` +where `x0` is the start position and `x1` is the end of the line. The `name`, `type`, `h`, and `w` +parameters are the same as for a `RefinementCenter`. + +To add a refinement region to the project, +``` + [Return:nothing] addRefinementRegion!(proj::Project, r::Dict{String,Any}) +``` + +To get a reference to a refinement region with a given name, use +``` + [Return:Dict{String,Any}] getRefinementRegion(proj::Project, name::String) +``` + +Finally, to get a list of all the refinement regions, +``` + [Return:Array{Dict{String,Any}}] array = allRefinementRegions(proj::Project) +``` + +A refinement region can be edited by using the following: +``` + [Return:nothing] setRefinementType!(r::Dict{String,Any}, type::String) + [Return:String] getRefinementType(r::Dict{String,Any}) + [Return:nothing] setRefinementLocation!(r::Dict{String,Any}, x::Array{Float64}) + [Return:Array{Float64}] getRefinementLocation(r::Dict{String,Any}) + [Return:nothing] setRefinementGridSize!(r::Dict{String,Any}, h::Float64) + [Return:Float64] getRefinementGridSize(r::Dict{String,Any}) + [Return:nothing] setRefinementWidth!(r::Dict{String,Any}, w::Float64) + [Return:Float64] getRefinementWidth(r::Dict{String,Any}) +``` +where `r` is a dictionary returned by `newRefinementCenter!`, `newRefinementLine!`, or `getRefinementRegion`. + +To further edit a `RefinementLine`, use the methods +``` + [Return:nothing] setRefinementStart!(r::Dict{String,Any}, x::Array{Float64}) + [Return:Array{Float64}] getRefinementStart(r::Dict{String,Any}) + [Return:nothing] setRefinementEnd!(r::Dict{String,Any}, x::Array{Float64}) + [Return:Array{Float64}] getRefinementEnd(r::Dict{String,Any}) +``` + +## Boundary curves + +The `Project` contains a model that contains information about any inner and/or outer +boundary curves that a user can add to define a domain to be meshed. Each curve is defined as a +"chain" that can be built from multiple connected curves. More details on boundary curves and +HOHQMesh can be found [here](https://trixi-framework.github.io/HOHQMesh/the-model/). + +The domain can have a single outer boundary chain and an arbitrary number of inner boundary chains. + +The orientation of any curve chains must be counter-clockwise. +This orientation is automatically checked in `generate_mesh` and a warning is thrown if +a user attempts to connect the curve chain in an invalid way. + +See the tutorial [Creating and editing curves](@ref) for a demonstration of defining, +constructing, and removing curves from a `Project`. + +### Defining curves + +Four curve types can be added to the outer and inner boundary curve chains. They are + +- Parametric equations +- Cubic Splines +- Lines defined by their end points +- Circular arcs + +#### Parametric equations + +Creating a new curve equation +``` + [Return:Dict{String,Any}] newParametricEquationCurve(name::String, + xEqn::String, + yEqn::String, + zEqn::String = "z(t) = 0.0") +``` +Returns a new set of parametric equation. Equations must be of the form +``` + () = ... +``` +The name of the function, and the argument are arbitrary. The equation can be any legitimate equation. +The constant `pi` is defined for use. Exponentiation is done with `^`. All number literals are interpreted +as floating point numbers. + +Example: +```julia + xEqn = "x(t) = 4*cos(2*pi*t) - 0.6*cos(8*pi*t)^3" + yEqn = "y(t) = 4*sin(2*pi*t) - 0.5*sin(11*pi*t)^2" + zEqn = "z(t) = 0.0" + blob = newParametricEquationCurve("Blob", xEqn, yEqn, zEqn) +``` +The z-Equation is optional, but for now must define zero for z by default. + +#### Cubic spline curve + +A cubic spline is defined by an array of knots, $t_j$, $x_j$, $y_j$, $z_j$. +It can either be supplied by a data file whose first line is the number of knots, and succeeding lines define +the $t$, $x$, $y$, $z$ values, e.g. +``` + 9 + 0.000000000000000 -3.50000000000000 3.50000000000000 0.0 + 3.846153846153846E-002 -3.20000000000000 5.00000000000 0.0 + 7.692307692307693E-002 -2.00000000000000 6.00000000000 0.0 + 0.769230769230769 0.000000000000000 -1.00000000000000 0.0 + 0.807692307692308 -1.00000000000000 -1.00000000000000 0.0 + 0.846153846153846 -2.00000000000000 -0.800000000000000 0.0 + 0.884615384615385 -2.50000000000000 0.000000000000000 0.0 + 0.923076923076923 -3.00000000000000 1.00000000000000 0.0 + 1.00000000000000 -3.50000000000000 3.50000000000000 0.0 +``` +or by constructing the required `nKnots` x `4` array and supplying it to the new procedure. The respective constructors +for a spline curve are +``` + [Return:Dict{String,Any}] newSplineCurve(name::String, + dataFile::String) + [Return:Dict{String,Any}] newSplineCurve(name::String, + nKnots::Int, + data::Matrix{Float64}) +``` +If the spline curve is to be closed. The last data point must be the same as the first. + +Example: +```julia + spline_data = [ [0.0 1.75 -1.0 0.0] + [0.25 2.1 -0.5 0.0] + [0.5 2.7 -1.0 0.0] + [0.75 0.6 -2.0 0.0] + [1.0 1.75 -1.0 0.0] ] + ex_spline = newSplineCurve("small_spline", 5, spline_data) +``` + +#### Line defined by end points + +A straight line is constructed with +``` + [Return:Dict{String,Any}] newEndPointsLineCurve(name::String, + xStart::Array{Float64}, + xEnd::Array{Float64}) +``` +The `xStart` and `xEnd` are arrays of the form [x, y, z]. The `z` component should be zero and for now is ignored. + +Example: +```julia + line1 = newEndPointsLineCurve("line_segment", [0.0, -3.0, 0.0], [1.0, 0.0, 0.0]) +``` + +#### Circular arc +``` + [Return:Dict{String,Any}] newCircularArcCurve(name::String, + center::Array{Float64}, + radius::Float64, + startAngle::Float64, + endAngle::Float64, + units::String) +``` +The center is an array of the form [x, y, z]. The units argument defines the start and end angle units. +It is either "degrees" or "radians". That argument is optional, and defaults to "degrees". + +Example: +```julia + halfCircle = newCircularArcCurve("Dome", [0.0, 0.0, 0.0], 1.0, 0.0, 180.0, "degrees") +``` + +### Adding and removing outer and inner boundaries + +1. Adding an outer boundary curve + + Using the curve creation routines described above, create curves counter-clockwise along the outer boundary + and add them to the outer boundary curve using + + ``` + [Return:nothing] addCurveToOuterBoundary!(proj::Project, + crv::Dict{String,Any}) + ``` + + `crv` is the dictionary that represents the curve. + + Example: + + ```julia + circ = newCircularArcCurve("outerCircle", [0.0, -1.0, 0.0], 4.0, 0.0, 360.0, "degrees") + addCurveToOuterBoundary!(p, circ) + ``` + +2. Adding an inner boundary curve + + The syntax is analogous to the creation of an outer boundary curve. Once interior curves are defined + they can be added to chain, again in counter-clockwise orientation. Note that the individual pieces + of the curve are given a name. The entire chain is also given a name. + + ``` + [Return:nothing] addCurveToInnerBoundary!(proj::Project, + crv::Dict{String,Any}, + chainName::String) + ``` + + Example: + + ```julia + line1 = newEndPointsLineCurve("line_segment", [0.0, -3.0, 0.0], [1.0, 0.0, 0.0]) + addCurveToInnerBoundary!(p, line1, "interior_curve") + ``` + + To edit curves they can be accessed by the name: + + ``` + [Return:Dict{String,Any}] getInnerBoundaryCurve(proj::Project, + curveName::String, + chainName::String) + [Return:Dict{String,Any}] getOuterBoundaryCurveWithName(proj::Project, + name::String) + ``` + +3. Deleting boundary curves + + The entire outer boundary or an entire inner boundary can be removed from the project. + + ``` + [Return:nothing] removeOuterBoundary!(proj::Project) + [Return:nothing] removeInnerBoundary!(proj::Project, chainName::String) + ``` + + Alternatively, individual pieces of the boundary curve chains can be removed. + + ``` + [Return:nothing] removeOuterBoundaryCurveWithName!(proj::Project, name::String) + [Return:nothing] removeInnerBoundaryCurve!(proj::Project, + name::String, + chainName::String) + ``` + + As in HOHQMesh the project can have only one outer boundary chain, so the removal does not + require a specific `chainName`. + +### Editing curves + +You can determine the type of a curve by +``` + [Return:String] getCurveType(crv::Dict{String,Any}) +``` + +For any of the curves, their name can be changed by +``` + setCurveName!(crv::Dict{String,Any}, name::String) +``` +and checked by +``` + getCurveName(crv::Dict{String,Any}) +``` + +Otherwise there are special functions to change the parameters of curves +``` + [Return:nothing] setXEqn!(crv::Dict{String,Any}, eqn::String) + [Return:nothing] setYEqn!(crv::Dict{String,Any}, eqn::String) + [Return:nothing] setZEqn!(crv::Dict{String,Any}, eqn::String) + [Return:nothing] setStartPoint!(crv::Dict{String,Any}, point::Array{Float64}) + [Return:nothing] setEndPoint!(crv::Dict{String,Any}, point::Array{Float64}) + [Return:nothing] setArcUnits!(arc::Dict{String,Any}, units::String) + [Return:nothing] setArcCenter!(arc::Dict{String,Any}, point::Array{Float64}) + [Return:nothing] setArcStartAngle!(arc::Dict{String,Any}, angle::Float64) + [Return:nothing] setArcEndAngle!(arc::Dict{String,Any}, angle::Float64) + [Return:nothing] setArcRadius!(arc::Dict{String,Any}, radius::Float64) + + [Return:String] getXEqn(crv::Dict{String,Any}) + [Return:String] getYEqn(crv::Dict{String,Any}) + [Return:String] getZEqn(crv::Dict{String,Any}) + [Return:Array{Float64}] getStartPoint(crv::Dict{String,Any}) + [Return:Array{Float64}] getEndPoint(crv::Dict{String,Any}) + [Return:String] getArcUnits(arc::Dict{String,Any}) + [Return:Array{Float64}] getArcCenter(arc::Dict{String,Any}) + [Return:Float64] getArcStartAngle(arc::Dict{String,Any}) + [Return:Float64] getArcEndAngle(arc::Dict{String,Any}) + [Return:Float64] getArcRadius(arc::Dict{String,Any}) +``` + +## Undo/redo + +The interactive mesh functionality has unlimited undo/redo for most actions. + +In interactive mode, actions can be undone by the commands +``` + [Return:String] undo() + [Return:String] redo() +``` +where the return string contains the name of the action performed. + +To find out what the next actions are, use +``` + [Return:String] undoActionName() + [Return:String] redoActionName() +``` + +Finally, to clear the undo stack, use +``` + [Return:nothing] clearUndoRedo() +``` \ No newline at end of file diff --git a/docs/src/interactive_overview.md b/docs/src/interactive_overview.md new file mode 100644 index 00000000..52524826 --- /dev/null +++ b/docs/src/interactive_overview.md @@ -0,0 +1,152 @@ +# [Overview](@id InteractiveTool) + +The interactive functionality is an API to generate a quadrilateral (future: hexahedral) +mesh using Julia. +It serves as a front end to the HOHQMesh program, and is designed to let one build a +meshing project interactively while graphically displaying the results. + +Several scripts are available in the `examples` folder +to get you started. These example scripts follow the naming convention of `interactive_*` where +the phrase interactive indicates their association with this API and then trailing information +will indicate what that script demonstrates. For instance, the file `interactive_spline_curves.jl` +provides an interactive project that creates an manipulates splines for the inner boundaries before +generating the mesh. + +Below we provide a broad overview of the interactive mesh workflow. Further clarification on +this workflow is provided in the [Guided tour](@ref). Several [Tutorials](@ref) +are also available to demonstrate this functionality. + +## Workflow and basic moves + +The order of the workflow and basic moves follow a logical pattern: The project must be created first. +Curves can be added at any time. The background grid can be added any time to the project. +A mesh is usually generated after the model (curves) and background grid are completed. + +To generate a mesh interactively you + +1. Create a project with a user given `projectName` and `folder` where any generated files are to be saved + + ``` + p = newProject(, ) + ``` + + Both of these input arguments are strings. + +2. Create inner and outer boundary curves from the available types + + ``` + c = newEndPointsLineCurve(, startLocation [x, y, z], endLocation [x, y, z]) *Straight Line* + c = newCircularArcCurve(, center [x, y, z], radius, startAngle, endAngle, units = "degrees" or "radians") *Circular Arc* + c = newParametricEquationCurve(, xEqn, yEqn, zEqn) *Parametric equation* + c = newSplineCurve(, dataFile) *Spline with data from a file* + c = newSpline(, nKnots, knotsMatrix) *Spline with given knot values* + ``` + + See [Defining curves](@ref) for further details on the different curve type currently supported by HOHQMesh. + + The generic name for each of these curve creation methods is `new!`. The generic can be used instead of the longer descriptive name to save typing during interactive sessions, if desired. + +3. Add curves to build the model to see what you have added, + + ``` + addOuterBoundaryCurve!(p, ) *Add outer boundary curve* + addInnerBoundaryCurve!(p, , ) *Add curve to an inner boundary* + ``` + + For a single inner / outer boundary curve the command above directly adds the curve into the `Project`. + If the inner / outer boundary curve is a chain of multiple curves then they must be added to the `Project` + in an order which yields a closed curves with counter-clockwise orientation. + See the [Guided tour](@ref) for an example of a chain of curves. + + Curves can be added by using the generic `add!` function instead of the longer descriptive + name to save typing during interactive sessions, if desired. + +4. Visualize the project's model, if desired + + ``` + plotProject!(p, MODEL) + ``` + + Plots are updated in response to user interactions. However, to update the plot at any time, use + + ``` + updatePlot!(p, options) + ``` + + Options are `MODEL`, `GRID`, `MESH`, and `REFINEMENTS`. To plot combinations, sum the options, e.g. + `MODEL`+`GRID` or `MODEL`+`MESH`. You normally are not interested in the background grid once + the mesh is generated. + + !!! note "Visualization requirement" + The interactive functionality uses [Makie.jl](https://github.com/JuliaPlots/Makie.jl/) + to visualize the `Project` information. Therefore, in addition to HOHQMesh.jl a user must + load a Makie backend (for example, [GLMakie](https://github.com/JuliaPlots/GLMakie.jl/) or + [CairoMakie](https://github.com/JuliaPlots/CairoMakie.jl)) if visualization is desired. + +5. Set the background grid + + When no outer boundary curve is present, the background grid can be set with + + ``` + addBackgroundGrid!(p, lower left [x,y,z], spacing [dx,dy,dz], num Intervals [nX,nY,nZ]) + ``` + + Or + + ``` + addBackgroundGrid!(p, [top value, left value, bottom value, right value], num Intervals [nX,nY,nZ]) + ``` + + The first method creates the rectangular boundary with extent `[x0[1], x0[1] + N*dx[1]]` by + `[x0[2], x0[2] + N*dx[2]]`. The second method sets a rectangular bounding box with extent + [top value, left value, bottom value, right value] and the number of elements in each direction. + The first exists for historical reasons; the second is probably the easiest to use. + + When an outer boundary is present the background grid can be set as + + ``` + addBackgroundGrid!(p, [dx, dy, dz]) + ``` + + where the spacing controls the number of elements in each direction. + + !!! note "Background grid" + A background grid is required by HOHQMesh. If one is not present in the `Project` + and a user attempts to generate the mesh a warning is thrown. + +6. Adjust meshing parameters, if desired. For instance, one can adjust the polynomial + `order` in the `Project` for any curved boundaries by + + ``` + setPolynomialOrder!(p, order) + ``` + + The background grid size can be adjusted where we can set the grid size in the x and y directions, + `dx` and `dy`, can be set separately + + ``` + setBackgroundGridSize!(p, 0.5, 0.25) + ``` + + See [Controlling the mesh generation](@ref) for details on adjusting parameters already present + in the `Project`. + +7. Generate the mesh + + ``` + generate_mesh(p) + ``` + + The mesh file will be saved in `` with the name `.mesh`. A HOHQMesh control file + is automatically created from the contents of the `Project` and is also saved in that folder + with the name `.control`. This control file can be read in again later and modified, + remeshed, etc. The function `generate_mesh` will print the mesh information and statistics, and will + plot the mesh as in the figure above, if a plot is otherwise visible. + If not, it can always be plotted with the `plotProject!` command. + +## Advanced + +All objects and information contained in the variable type `Project` are actually dictionaries of type `Dict{String, Any}`. +Since Julia is not an object oriented language, the parameters and other parts of these internal dictionaries +can be accessed and edited directly by key and value. +However, if you do that, then certain features like `undo`/`redo` and automatic plot updating **will not work**. \ No newline at end of file diff --git a/docs/src/license.md b/docs/src/license.md index b696e593..b6038dab 100644 --- a/docs/src/license.md +++ b/docs/src/license.md @@ -1,19 +1,19 @@ # License > MIT License -> -> Copyright (c) 2021-present Michael Schlottke-Lakemper -> +> +> Copyright (c) 2021-present David Kopriva, Andrew Winters, and Michael Schlottke-Lakemper +> > Permission is hereby granted, free of charge, to any person obtaining a copy > of this software and associated documentation files (the "Software"), to deal > in the Software without restriction, including without limitation the rights > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell > copies of the Software, and to permit persons to whom the Software is > furnished to do so, subject to the following conditions: -> +> > The above copyright notice and this permission notice shall be included in all > copies or substantial portions of the Software. -> +> > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE diff --git a/docs/src/testing.md b/docs/src/testing.md new file mode 100644 index 00000000..937a521d --- /dev/null +++ b/docs/src/testing.md @@ -0,0 +1,85 @@ +# Testing + +During the development of HOHQMesh and its interactive functionality, we rely on +[continuous testing](https://en.wikipedia.org/wiki/Continuous_testing) to ensure +that modifications or new features do not break existing +functionality or add other errors. In the main +[HOHQMesh](https://github.com/trixi-framework/HOHQMesh.jl) repository, this is facilitated by +[GitHub Actions](https://docs.github.com/en/free-pro-team@latest/actions), +which allows to run tests automatically upon certain events. When, how, and what +is tested by GitHub Actions is controlled by the workflow file +[`.github/workflows/ci.yml`](https://github.com/trixi-framework/HOHQMesh.jl/blob/main/.github/workflows/ci.yml). +In Trixi and its related repositories, tests are triggered by +* each `git push` to `main` and +* each `git push` to any pull request. +Besides checking functionality, we also analyze the [Test coverage](@ref) to +ensure that we do not miss important parts during testing. + +!!! note "Test and coverage requirements" + Before merging a pull request (PR) to `main`, we require that + * the code passes all functional tests + * code coverage does not decrease. + + +## Testing setup +The entry point for all testing is the file +[`test/runtests.jl`](https://github.com/trixi-framework/HOHQMesh.jl/blob/main/test/runtests.jl), +which is run by the automated tests and which can be triggered manually by +executing +```julia +julia> using Pkg; Pkg.test("HOHQMesh") +``` +in the REPL. Since there already exist many tests, we have split them up into +multiple files in the `test` directory to allow for faster testing of individual +parts of the code. +Thus in addition to performing all tests, you can also just `include` one of the +files named `test_xxx.jl` to run only a specific subset, e.g., +```julia +julia> # Run all test for the interactive Curve API + include(joinpath("test", "test_curve.jl")) +``` + + +## Adding new tests +We use Julia's built-in [unit testing capabilities](https://docs.julialang.org/en/v1/stdlib/Test/) +to configure tests. In general, newly added code must be covered by at least one +test, and all new scripts added to the `examples/` directory must be used at +least once during testing. New tests should be added to the corresponding +`test/test_xxx.jl` file, e.g., a test involving visualization capabilities +would go into +[`test/test_visualization.jl`](add link). +Please study one of the existing tests and stay consistent to the current style +when creating new tests. + +Since we want to test as much as possible, we have a lot of tests and +frequently create new ones. Therefore, new tests should be as +short as reasonably possible, i.e., without being too insensitive to pick up +changes or errors in the code. + +When you add new tests, please check whether all CI jobs still take approximately +the same time. If the job where you added new tests takes much longer than +everything else, please consider moving some tests from one job to another +(or report this incident and ask the main developers for help). + +!!! note "Test duration" + As a general rule, tests should last **no more than 10 seconds** when run + with a single thread and after compilation (i.e., excluding the first run). + + +## Test coverage +In addition to ensuring that the code produces the expected results, the +automated tests also record the +[code coverage](https://en.wikipedia.org/wiki/Code_coverage). The resulting +coverage reports, i.e., which lines of code were executed by at least one test +and are thus considered "covered" by testing, are automatically uploaded to +[Coveralls](https://coveralls.io) for easy analysis. Typically, you see a number +of Coveralls results at the bottom of each pull request: One for each parallel +job (see [Testing setup](@ref)), which can usually be ignored since they only +cover parts of the code by definition, and a cumulative coverage result named +`coverage/coveralls`. The "Details" link takes you to a detailed report on +which lines of code are covered by tests, which ones are missed, and especially +which *new* lines the pull requests adds to HOHQMesh's code base that are not yet +covered by testing. +!!! note "Coverage requirements" + In general, we require pull requests to *not decrease* the overall + test coverage percentage in `main`, with a **hard lower bound of 97%**. diff --git a/docs/src/tutorials/create_edit_curves.md b/docs/src/tutorials/create_edit_curves.md new file mode 100644 index 00000000..7afda25c --- /dev/null +++ b/docs/src/tutorials/create_edit_curves.md @@ -0,0 +1,527 @@ +# Creating and editing curves + +The purpose of this tutorial is to demonstrate how to inner and outer boundary +curve chains. +By a "chain" we mean a closed curve that is composed of multiple pieces. +Each chain can be a combination of different curve types, e.g., a circular +arc can connect to a spline. +It also shows how to modify, remove, and add new pieces to an existing curve chain. +The `undo` and `redo` capabilities of the interactive mesh tool are briefly discussed. +The outer and inner boundary curves, background grid as well as the mesh +will be visualized for quality inspection. + +### Synopsis + +This tutorial demonstrates how to: +* Create and edit an outer boundary chain. +* Create and edit an inner boundary chain. +* Add the background grid when an outer boundary curve is present. +* Visualize an interactive mesh project. +* Discuss undo / redo capabilities. +* Construct and add parametric spline curves. +* Construct and add a curve from parametric equations. +* Construct and add straight line segments. +* Construct and add circular arc segments. + +## Initialization + +From a Julia REPL we load the HOHQMesh package as well as +[GLMakie](https://github.com/JuliaPlots/GLMakie.jl/), a backend of +[Makie.jl](https://github.com/JuliaPlots/Makie.jl/), to visualize the +curves, mesh, etc. from the interactive tool. +```julia +julia> using GLMakie, HOHQMesh +``` +Now we are ready to interactively generate unstructured quadrilateral meshes! + +We create a new project with the name `"sandbox"` and +assign `"out"` to be the folder where any output files from the mesh generation process +will be saved. By default, the output files created by HOHQMesh will carry the same name +as the project. For example, the resulting HOHQMesh control file from this tutorial +will be named `sandbox.control`. +If the folder `out` does not exist, it will be created automatically in +the current file path. +```julia +sandbox_project = newProject("sandbox", "out") +``` + +## Add the outer boundary chain + +We first create the outer boundary curve chain that is composed of three pieces +1. Straight line segment from $(0, -7)$ to $(5, 3)$. +2. Half-circle arc of radius $r=5$ centered at $(0, 3)$. +3. Straight line segment from $(-5, 3)$ to $(0, -7)$. + +Each segment of the curve is created separately. The straight line segments are +made with the function `newEndPointsLineCurve` and given unique names: +```julia +outer_line1 = newEndPointsLineCurve("Line1", # curve name + [0.0, -7.0, 0.0], # start point + [5.0, 3.0, 0.0]) # end point + +outer_line2 = newEndPointsLineCurve("Line2", # curve name + [-5.0, 3.0, 0.0], # start point + [0.0, -7.0, 0.0]) # end point +``` +To create the circle arc we use the function `newCircularArcCurve` where +we specify a name for the curve as well as the radius and center of the circle. +The arc can have an arbitrary length dictated by the start and end angle, e.g., for +a half-circle we take the angle to vary from $0$ to $180$ degrees. +```julia +outer_arc = newCircularArcCurve("Arc", # curve name + [0.0, 3.0, 0.0], # center + 5.0, # radius + 0.0, # start angle + 180.0, # end angle + "degrees") # units for angle +``` +We use `"degrees"` to set the angle bounds, but `"radians"` can also be used. +The name of the curve stored in the dictionary `outer_arc` is assigned to be `"Arc"`. + +The curve names `"Line1"`, `"Line2"`, and `"Arc"` are the labels that +HOHQMesh will give to these boundary curve segments in the resulting mesh file. + +The three curve segments stored in the variables `outer_line1`, `outer_line2`, and `outer_arc` +are then added to the `sandbox_project` with a counter-clockwise +orientation as required by HOHQMesh. +```julia +addCurveToOuterBoundary!(sandbox_project, outer_line1) +addCurveToOuterBoundary!(sandbox_project, outer_arc) +addCurveToOuterBoundary!(sandbox_project, outer_line2) +``` + +## Add a background grid + +HOHQMesh requires a background grid for the mesh generation process. This background grid sets +the base resolution of the desired mesh. HOHQMesh will automatically subdivide from this background +grid near sharp features of any curved boundaries. + +For a domain bounded by an outer boundary curve, this background grid is set by indicating +the desired element size in the $x$ and $y$ directions. +To start, we set the background grid for `sandbox_project` +to have elements with side length one in each direction +```julia +addBackgroundGrid!(sandbox_project, [1.0, 1.0, 0.0]) +``` + +We visualize the outer boundary curve chain and background grid with the following +```julia +plotProject!(sandbox_project, MODEL+GRID) +``` +Here, we take the sum of the keywords `MODEL` and `GRID` in order to simultaneously visualize +the outer boundary and background grid. The resulting plot is given below. The chain of outer boundary +curves is called `"Outer"` and it contains three curve segments `"Line1"`, `"Arc"`, and `"Line2"` +labeled in the figure by `O.1`, `O.2`, and `O.3`, respectively. + +![background_grid](https://user-images.githubusercontent.com/25242486/175062627-a87ed6e1-ce68-4ef4-a178-96b1ccceff0a.png) + +## Edit the outer boundary chain + +Suppose that the domain boundary requires a curved segment instead of the straight line +`"Line2"`. We will replace this line segment in the outer boundary chain with a cubic +spline. + +First, we remove the `"Line2"` curve from the `"Outer"` chain with the command +```julia +removeOuterBoundaryCurveWithName!(sandbox_project, "Line2") +``` +!!! tip "Outer curve removal" + Alternatively, we can remove the curve `"Line2"` using its index in the `"Outer"` boundary + chain. + ```julia + removeOuterBoundaryCurveAtIndex!(sandbox_project, 3) + ``` + This removal strategy is useful when the curves in the boundary chain do not have unique + names. Be aware that when curves are removed from a chain it is possible that + the indexing of the remaining curves changes. +The plot automatically updates and we see that the outer boundary is open and contains +two segments: `"Line1"` and `"Arc"`. + +![outer_removal](https://user-images.githubusercontent.com/25242486/175062663-8a0e3a4f-c444-4302-a35b-7565095ab78a.png) + +Next, we create a parametric cubic spline curve from a given set of data points. In order to make a +closed outer boundary chain the cubic spline must begin at the endpoint of the curve `"Arc"` +and end at the first point of the curve `"Line1"`. This ensures that the new spline curve +connects into the boundary curve chain with the correct orientation. To create a parametric +spline curve we directly provide data points in the code. These points take the form +`[t, x, y, z]` where `t` is the parameter variable that varies between $0$ and $1$. +The spline curve constructor `newSplineCurve` also takes the number of points as an +input argument. +```julia +spline_data = [ [0.0 -5.0 3.0 0.0] + [0.25 -2.0 1.0 0.0] + [0.5 -4.0 0.5 0.0] + [0.75 -2.0 -3.0 0.0] + [1.0 0.0 -7.0 0.0] ] +outer_spline = newSplineCurve("Spline", 5, spline_data) +``` +Now we add the spline curve `outer_spline` into the `sandbox_project`. +```julia +addCurveToOuterBoundary!(sandbox_project, outer_spline) +``` +The figure updates automatically to display the `"Outer"` boundary chain +with the new `"Spline"` curve labeled `O.3`. + +![outer_spline](https://user-images.githubusercontent.com/25242486/175062706-6d12840c-f5fa-49e6-ad18-d46d643dd3d8.png) + +## Add an inner boundary chain + +We create a pill shaped inner boundary curve chain composed of four pieces +1. Straight line segment from $(1, 5)$ to $(1, 3)$. +2. Half-circle arc of radius $r=1$ centered at $(0, 3)$. +3. Straight line segment from $(-1, 3)$ to $(-1, 5)$. +4. Half-circle arc of radius $r=1$ centered at $(0, 5)$. + +Similar to the construction of the `"Outer"` boundary chain, each segment of +this inner boundary chain is created separately. The straight line segments are +made with the function `newEndPointsLineCurve` and given unique names: +```julia +inner_line1 = newEndPointsLineCurve("Line1", # curve name + [1.0, 5.0, 0.0], # start point + [1.0, 3.0, 0.0]) # end point + +inner_line2 = newEndPointsLineCurve("Line2", # curve name + [-1.0, 3.0, 0.0], # start point + [-1.0, 5.0, 0.0]) # end point +``` +To create the circle arcs we use the function `newCircularArcCurve` where +we specify a name for the curve as well as the radius and center of the circle. +In order to create an inner curve chain with counter-clockwise orientation the +angle for the bottom half-circle arc centered at $(0, 3)$ varies from $0$ to $-180$ +degrees. The top half-circle arc centered at $(0, 5)$ has an angle that varies from +$180$ to $0$ degrees. The construction of the two circle arcs are +```julia +inner_bottom_arc = newCircularArcCurve("BottomArc", # curve name + [0.0, 3.0, 0.0], # center + 1.0, # radius + 0.0, # start angle + -pi, # end angle + "radians") # units for angle + +inner_top_arc = newCircularArcCurve("TopArc", # curve name + [0.0, 5.0, 0.0], # center + 1.0, # radius + 180.0, # start angle + 0.0, # end angle + "degrees") # units for angle +``` +Note, we use `"radians"` to set the angle bounds for `inner_bottom_arc` and `"degrees"` +for the angle bounds of `inner_top_arc`. + +The curve names `"Line1"`, `"Line2"`, `"BottomArc"`, and `"TopArc"` are the labels that +HOHQMesh will give to these inner boundary curve segments in the resulting mesh file. + +The four curve segments stored in the variables `inner_line1`, `inner_line2`, +`inner_bottom_arc` and `outer_arc` are added to the `sandbox_project` in counter-clockwise order as required by HOHQMesh. +```julia +addCurveToInnerBoundary!(sandbox_project, inner_line1, "inner") +addCurveToInnerBoundary!(sandbox_project, inner_bottom_arc, "inner") +addCurveToInnerBoundary!(sandbox_project, inner_line2, "inner") +addCurveToInnerBoundary!(sandbox_project, inner_top_arc, "inner") +``` +This inner boundary chain name `"inner"` is used internally by HOHQMesh. The visualization +of the background grid automatically detects that curves have been added to the +`sandbox_project` and the plot is updated, as shown below. The chain for the inner boundary curve chain is called `inner` and it contains a four curve segments +`"Line1"`, `"BottomArc"`, `"Line2"`, and `"TopArc"` labeled in the figure by +`1.1`, `1.2`, `1.3`, and `1.4`, respectively. + +![inner_pill](https://user-images.githubusercontent.com/25242486/175062834-91aae24d-167d-4d0f-8d39-887ab189081b.png) + +## Generate the mesh + +We next generate the mesh from the information contained in the `sandbox_project`. +This will output the following files to the `out` folder: + +* `sandbox.control`: A HOHQMesh control file for the current project. +* `sandbox.tec`: A TecPlot formatted file to visualize the mesh with other software, e.g., [ParaView](https://www.paraview.org/). +* `sandbox.mesh`: A mesh file with format `ISM-V2` (the default format). + +To do this we execute the command +```julia +generate_mesh(sandbox_project) + + ******************* + 2D Mesh Statistics: + ******************* + Total time = 8.7928000000000006E-002 + Number of nodes = 513 + Number of Edges = 933 + Number of Elements = 422 + Number of Subdivisions = 7 + + Mesh Quality: + Measure Minimum Maximum Average Acceptable Low Acceptable High Reference + Signed Area 0.00003020 1.17336756 0.18813064 0.00000000 999.99900000 1.00000000 + Aspect Ratio 1.00984888 2.32321419 1.31488869 1.00000000 999.99900000 1.00000000 + Condition 1.00041121 2.42894151 1.21101797 1.00000000 4.00000000 1.00000000 + Edge Ratio 1.01674110 3.74861238 1.59495734 1.00000000 4.00000000 1.00000000 + Jacobian 0.00001734 1.13821390 0.14136293 0.00000000 999.99900000 1.00000000 + Minimum Angle 32.20087774 89.35157729 68.75755243 40.00000000 90.00000000 90.00000000 + Maximum Angle 90.60787193 152.53515465 113.36966060 90.00000000 135.00000000 90.00000000 + Area Sign 1.00000000 1.00000000 1.00000000 1.00000000 1.00000000 1.00000000 +``` +The call to `generate_mesh` also prints mesh quality statistics to the screen +and updates the visualization. +The background grid is *removed* from the visualization when the mesh is generated. + +!!! note "Mesh visualization" + Currently, only the "skeleton" of the mesh is visualized. Thus, the high-order curved boundary information + is not seen in the plot but this information **is present** in the generated mesh file. + +![initial_mesh](https://user-images.githubusercontent.com/25242486/175062901-4f1280ae-9830-4ab3-bee2-76f895b03cbb.png) + +## Delete the existing mesh + +In preparation of edits we will make to the inner boundary chain we remove the current mesh from the plot +and re-plot the model curves and background grid. +Note, this step is not required, but it helps avoid confusion when editing several curves. +```julia +remove_mesh!(sandbox_project) +updatePlot!(sandbox_project, MODEL+GRID) +``` +Additionally, the `remove_mesh!` command deletes the mesh information from the `sandbox_project` +and `sandbox.mesh` from the `out` folder. However, the `sandbox.control` and `sandbox.tec` files +are still present in `out` directory. + +## Edit an inner boundary chain + +Suppose that the inner boundary actually requires a curved segment instead of the +straight line `"Line1"`. We will replace this line segment in the inner boundary chain +with an oscillating segment construct from a set of parametric equations. In doing so, +it will also be necessary to remove the `BottomArc` and replace it with a new, wider +circular arc segment. + +We remove the `"Line1"` curve from the `inner` chain with the command +```julia +removeInnerBoundaryCurve!(sandbox_project, "Line1", "inner") +``` +!!! tip "Inner curve removal" + Alternatively, we can remove the curve `"Line1"` using its index in the `inner` boundary + chain. + ```julia + removeInnerBoundaryCurveAtIndex!(sandbox_project, 1, "inner") + ``` + This removal strategy is useful when the curves in the boundary chain do not have unique + names. Be aware that when curves are removed from a chain it is possible that + the indexing of the remaining curves changes. +With either removal strategy, the plot automatically updates. We see that the +inner boundary is open and contains three segments: `"BottomArc"`, `"Line2"`, and `"TopArc"`. +Note that the index of the remaining curves has changed as shown below. + +![inner_removal](https://user-images.githubusercontent.com/25242486/175062997-6f60b3e3-b9eb-4f6b-8062-5b17de0cca2c.png) + +!!! note "Brief note about undo / redo" + The interactive functionality (globally) carries an operation stack of actions that can be undone + (or redone) as the case may be. We can query and print to the REPL the top of the + undo stack with `undoActionName`. + ```julia + undoActionName() + "Remove Inner Boundary Curve" + ``` + We can undo the removal of the `"Line1"` curve with `undo` + ```julia + undo() + "Undo Remove Inner Boundary Curve" + ``` + In addition to reinstating `"Line1"` into the `sandbox_project`, this undo + prints the action that was undone to the REPL and will update the figure. + + Analogously, there is a redo operation stack. We query and print to the REPL the top + the redo stack with `redoActionName` and can use `redo` to perform + the operation. + +The new inner curve segment will be an oscillating line given by the +parametric equations +```math + \begin{aligned} + x(t) &= t + 1,\\[0.2cm] + y(t) &= -2t + 5 - \frac{3}{2} \cos(\pi t) \sin(\pi t),\\[0.2cm] + z(t) &= 0 + \end{aligned} + \qquad + t\in[0,1] +``` +Parametric equations in HOHQMesh can be any legitimate equation and use intrinsic functions +available in Fortran, e.g., $\sin$, $\cos$, exp. +The constant `pi` is available for use. +The following commands create a new curve for the parametric equations above +```julia +xEqn = "x(t) = t + 1" +yEqn = "y(t) = -2 * t + 5 - 1.5 * cos(pi * t) * sin(pi * t)" +zEqn = "z(t) = 0.0" +inner_eqn = newParametricEquationCurve("wiggleLine", xEqn, yEqn, zEqn) +``` +The name of this new curve is assigned to be `"wiggleLine"`. +We add this new curve to the `"inner"` chain. +```julia +addCurveToInnerBoundary!(sandbox_project, inner_eqn, "inner") +``` +The automatically updated figure now shows: + +![inner_open_chain](https://user-images.githubusercontent.com/25242486/175063103-e8eda78c-b0d9-4229-9383-5582845d5f81.png) + +We see from the figure that this parametric equation curve starts at the point $(1,5)$ +and, therefore, matches the end point of the existing curve `"TopArc"` present +in the `"inner"` chain. However, the +parametric equation curve ends at the point $(2,3)$ which **does not** match +the `"BottomArc"` curve. So, the inner boundary chain remains open. + +!!! warning "Attempt to generate a mesh with an open curve chain" + An open curve chain is **invalid** in HOHQMesh. All inner and/or outer curve chains + must be closed. If we attempt to send a project that contains an open + curve chain to `generate_mesh` a warning is thrown and no mesh or output files + are generated. + +To create a closed boundary curve we must remove the `"BottomArc"` curve and replace it +with a wider half-circle arc segment. This new half-circle arc must start at the point +$(2, 3)$ and end at the point $(-1, 3)$ to close the inner chain **and** guarantee the +chain is oriented counter-clockwise. So, we first remove the `"BottomArc"` from the `"inner"` +chain. +```julia +removeInnerBoundaryCurve!(sandbox_project, "BottomArc", "inner") +``` +The figure updates to display the `"inner"` curve chain with three segments. +Note that the inner curve chain indexing has, again, been automatically adjusted. + +![inner_remove_arc](https://user-images.githubusercontent.com/25242486/175063146-9475697a-3aa8-42c1-abdb-713343c6b8f7.png) + +A half-circle arc that joins the points $(2, 3)$ and $(-1, 3)$ has a radius $r=1.5$, is +centered at $(0.5, 3)$ and has an angle that vaires from $0$ to $-180$. +We construct this circle arc and directly add it to the `sandbox_project`. +```julia +new_bottom_arc = newCircularArcCurve("wideBottomArc", # curve name + [0.5, 3.0, 0.0], # center + 1.5, # radius + 0.0, # start angle + -pi, # end angle + "radians") # units for angle +addCurveToInnerBoundary!(sandbox_project, new_bottom_arc, "inner") +``` +The updated plot now gives the modified, closed inner curve chain that now contains +four curve segments `"Line2"`, `"TopArc"`, `"wiggleLine"`, and `"wideBottomArc"` labeled +in the figure by `1.1`, `1.2`, `1.3`, and `1.4`, respectively. + +![inner_modified](https://user-images.githubusercontent.com/25242486/175063184-9c2d1204-cdcd-4a33-88bd-d73e7183b3d6.png) + +## Regenerate the mesh + +With the modifications to the inner curve chain complete we can regenerate the mesh. +This will create a new `sandbox.mesh` file and overwrite the existing `sandbox.control` and `sandbox.tec` files in the `out` directory. +```julia +generate_mesh(sandbox_project) + + ******************* + 2D Mesh Statistics: + ******************* + Total time = 0.13299600000000000 + Number of nodes = 714 + Number of Edges = 1308 + Number of Elements = 596 + Number of Subdivisions = 7 + + Mesh Quality: + Measure Minimum Maximum Average Acceptable Low Acceptable High Reference + Signed Area 0.00003020 1.15662678 0.12823840 0.00000000 999.99900000 1.00000000 + Aspect Ratio 1.01082600 3.14765817 1.34292128 1.00000000 999.99900000 1.00000000 + Condition 1.00037252 2.59936116 1.22903490 1.00000000 4.00000000 1.00000000 + Edge Ratio 1.02724726 3.74861238 1.64807401 1.00000000 4.00000000 1.00000000 + Jacobian 0.00001734 1.13150266 0.09438571 0.00000000 999.99900000 1.00000000 + Minimum Angle 31.88018513 89.33451932 67.86550651 40.00000000 90.00000000 90.00000000 + Maximum Angle 90.43850948 157.31718198 114.36070355 90.00000000 135.00000000 90.00000000 + Area Sign 1.00000000 1.00000000 1.00000000 1.00000000 1.00000000 1.00000000 +``` +The visualization updates automatically and the background grid is *removed* after when the mesh is generated. + +![inner_modified](https://user-images.githubusercontent.com/25242486/175063283-b60d8985-0fce-4010-90c5-5a924883a895.png) + +Inspecting the mesh we see that the automatic subdivision in HOHQMesh does well to capture the sharp corners and fine features of the curved inner and outer boundaries. For example, we zoom +into sharp corner at the bottom of the domain and see that, although small, the elements in this +region maintain a good quadrilateral shape. + +![zoom_corner](https://user-images.githubusercontent.com/25242486/175063320-57d42322-2d0e-4e69-b177-ab40d6a8df3d.png) + +## Summary + +In this tutorial we demonstrated how to: +* Create and edit an outer boundary chain. +* Create and edit an inner boundary chain. +* Add the background grid when an outer boundary curve is present. +* Visualize an interactive mesh project. +* Discuss undo / redo capabilities. +* Construct and add parametric spline curves. +* Construct and add a curve from parametric equations. +* Construct and add straight line segments. +* Construct and add circular arc segments. + +For completeness, we include a script with all the commands to generate the mesh displayed in the final image. +Note, we **do not** include the plotting in this script. +```julia +# Interactive mesh with modified outer and inner curve chains +# +# Create inner / outer boundary chains composed of the four +# available HOHQMesh curve types. +# +# Keywords: outer boundary, inner boundary, parametric equations, +# circle arcs, cubic spline, curve removal +using HOHQMesh + +# Instantiate the project +sandbox_project = newProject("sandbox", "out") + +# Add the background grid +addBackgroundGrid!(sandbox_project, [1.0, 1.0, 0.0]) + +# Create and add the original outer boundary curves +outer_line1 = newEndPointsLineCurve("Line1", [0.0, -7.0, 0.0], [5.0, 3.0, 0.0]) +outer_line2 = newEndPointsLineCurve("Line2", [-5.0, 3.0, 0.0], [0.0, -7.0, 0.0]) +outer_arc = newCircularArcCurve("Arc", [0.0, 3.0, 0.0], 5.0, 0.0, 180.0, "degrees") + +addCurveToOuterBoundary!(sandbox_project, outer_line1) +addCurveToOuterBoundary!(sandbox_project, outer_arc) +addCurveToOuterBoundary!(sandbox_project, outer_line2) + +# Modify the outer boundary to have a spline instead of a straight line +removeOuterBoundaryCurveWithName!(sandbox_project, "Line2") + +spline_data = [ [0.0 -5.0 3.0 0.0] + [0.25 -2.0 1.0 0.0] + [0.5 -4.0 0.5 0.0] + [0.75 -2.0 -3.0 0.0] + [1.0 0.0 -7.0 0.0] ] +outer_spline = newSplineCurve("Spline", 5, spline_data) +addCurveToOuterBoundary!(sandbox_project, outer_spline) + +# Create and add the inner boundary curves +inner_line1 = newEndPointsLineCurve("Line1", [1.0, 5.0, 0.0], [1.0, 3.0, 0.0]) +inner_line2 = newEndPointsLineCurve("Line2", [-1.0, 3.0, 0.0], [-1.0, 5.0, 0.0]) +inner_bottom_arc = newCircularArcCurve("BottomArc", [0.0, 3.0, 0.0], 1.0, 0.0, -pi, "radians") +inner_top_arc = newCircularArcCurve("TopArc", [0.0, 5.0, 0.0], 1.0, 180.0, 0.0, "degrees") + +addCurveToInnerBoundary!(sandbox_project, inner_line1, "inner") +addCurveToInnerBoundary!(sandbox_project, inner_bottom_arc, "inner") +addCurveToInnerBoundary!(sandbox_project, inner_line2, "inner") +addCurveToInnerBoundary!(sandbox_project, inner_top_arc, "inner") + +# Generate a mesh +generate_mesh(sandbox_project) + +# Delete the existing mesh before modifying the inner boundary curve chain +remove_mesh!(sandbox_project) + +# Modify the inner boundary curve with an oscillatory line and a new circle arc +removeInnerBoundaryCurve!(sandbox_project, "Line1", "inner") +removeInnerBoundaryCurve!(sandbox_project, "BottomArc", "inner") + +xEqn = "x(t) = t + 1" +yEqn = "y(t) = -2 * t + 5 - 1.5 * cos(pi * t) * sin(pi * t)" +zEqn = "z(t) = 0.0" +inner_eqn = newParametricEquationCurve("wiggleLine", xEqn, yEqn, zEqn) + +new_bottom_arc = newCircularArcCurve("wideBottomArc", [0.5, 3.0, 0.0], 1.5, 0.0, -pi, "radians") + +addCurveToInnerBoundary!(sandbox_project, inner_eqn, "inner") +addCurveToInnerBoundary!(sandbox_project, new_bottom_arc, "inner") + +# Regenerate the final mesh +generate_mesh(sandbox_project) +``` \ No newline at end of file diff --git a/docs/src/tutorials/curved_outer_boundary.md b/docs/src/tutorials/curved_outer_boundary.md new file mode 100644 index 00000000..dfbaf3d2 --- /dev/null +++ b/docs/src/tutorials/curved_outer_boundary.md @@ -0,0 +1,240 @@ +# Curved outer boundary + +The purpose of this tutorial is to demonstrate how to create an unstructured mesh on +a domain with a curved outer boundary. This outer boundary curve is defined by +parametric equations and contains fine features as well as smooth regions. + +The outer boundary, background grid and mesh +are visualized for quality inspection. The tutorial also shows how to adjust the +background and add a local refinement region in order to better resolve a portion +of the curved boundary. + +### Synopsis + +This tutorial demonstrates how to: +* Define a curved outer boundary using parametric equations. +* Add and adjust the background grid. +* Visualize an interactive mesh project. +* Add manual refinement to a local region of the domain. + +## Initialization + +From a Julia REPL we load the HOHQMesh package as well as +[GLMakie](https://github.com/JuliaPlots/GLMakie.jl/), a backend of +[Makie.jl](https://github.com/JuliaPlots/Makie.jl/), to visualize the +boundary curve, mesh, etc. from the interactive tool. +```julia +julia> using GLMakie, HOHQMesh +``` +Now we are ready to interactively generate unstructured quadrilateral meshes! + +We create a new project with the name `"TheBlob"` and +assign `"out"` to be the folder where any output files from the mesh generation process +will be saved. By default, the output files created by HOHQMesh will carry the same name +as the project. For example, the resulting mesh file from this tutorial will be named +`TheBlob.mesh`. +If the folder `out` does not exist, it will be created automatically in +the current file path. +```julia +blob_project = newProject("TheBlob", "out") +``` + +## Add the outer boundary + +The outer boundary curve for the domain of interest in this tutorial is given by the +parametric equations +```math + \begin{aligned} + x(t) &= 4\cos(2 \pi t) - \frac{3}{5}\cos^3(8 \pi t),\\[0.2cm] + y(t) &= 4\sin(2 \pi t) - \frac{1}{2}\sin^2(11 \pi t),\\[0.2cm] + z(t) &= 0 + \end{aligned} + \qquad + t\in[0,1] +``` +Parametric equations in HOHQMesh can be any legitimate equation and use intrinsic functions +available in Fortran, e.g., $\sin$, $\cos$, exp. +The constant `pi` is available for use. Exponentiation is done with `^`. +All number literals are interpreted as floating point numbers. + +The following commands create a new curve for the parametric equations above +```julia +xEqn = "x(t) = 4 * cos(2 * pi * t) - 0.6 * cos(8 * pi * t)^3" +yEqn = "y(t) = 4 * sin(2 * pi * t) - 0.5 * sin(11* pi * t)^2" +zEqn = "z(t) = 0.0" +blob = newParametricEquationCurve("Blob", xEqn, yEqn, zEqn) +``` +The name of this curve is assigned to be `"Blob"`. This name is also the label that HOHQMesh +will give to this boundary curve in the resulting mesh file. + +Now that we have created the boundary curve it must be added as an outer boundary +in the `blob_project`. +```julia +addCurveToOuterBoundary!(blob_project, blob) +``` + +## Add a background grid + +HOHQMesh requires a background grid for the mesh generation process. This background grid sets +the base resolution of the desired mesh. HOHQMesh will automatically subdivide from this background +grid near sharp features of any curved boundaries. + +For a domain bounded by an outer boundary curve, this background grid is set by indicating the desired +element size in the $x$ and $y$ directions. To start, we set the background grid for `blob_project` to +have elements with side length two in each direction +```julia +addBackgroundGrid!(blob_project, [2.0, 2.0, 0.0]) +``` + +We next visualize the outer boundary curve and background grid with the following +```julia +plotProject!(blob_project, MODEL+GRID) +``` +Here, we take the sum of the keywords `MODEL` and `GRID` in order to simultaneously visualize +the curves and background grid. The resulting plot is given below. The chain of outer boundary +curves is called `"Outer"` and it contains a single curve `"Blob"` labeled in the figure by `O.1`. + +![coarse_grid](https://user-images.githubusercontent.com/25242486/174747035-f21bb8d1-386f-4036-b2e8-59e264c071d1.png) + +From the visualization we see that the background grid is likely too coarse to produce a "good" +quadrilateral mesh for this domain. We reset the background grid size to have elements with +size one half in each direction +```julia +setBackgroundGridSize!(blob_project, 0.5, 0.5) +``` +Note, that after we execute the command above the visualization updates automatically with the +outer boundary curve and the new background grid. + +![fine_grid](https://user-images.githubusercontent.com/25242486/174747046-f1bc9734-ef4e-4e4c-9055-c54ebe1537e7.png) + +The new background grid that gives a finer initial resolution looks suitable to continue +to the mesh generation. + +## Initial mesh and user adjustments + +We next generate the mesh from the information contained in the `blob_project`. +This will output the following files to the `out` folder: + +* `TheBlob.control`: A HOHQMesh control file for the current project. +* `TheBlob.tec`: A TecPlot formatted file to visualize the mesh with other software, e.g., [ParaView](https://www.paraview.org/). +* `TheBlob.mesh`: A mesh file with format `ISM-V2` (the default format). + +To do this we execute the command +```julia +generate_mesh(blob_project) + + ******************* + 2D Mesh Statistics: + ******************* + Total time = 0.10612399999999998 + Number of nodes = 481 + Number of Edges = 895 + Number of Elements = 417 + Number of Subdivisions = 5 + + Mesh Quality: + Measure Minimum Maximum Average Acceptable Low Acceptable High Reference + Signed Area 0.00025346 0.36181966 0.11936327 0.00000000 999.99900000 1.00000000 + Aspect Ratio 1.00002883 2.58066393 1.26340310 1.00000000 999.99900000 1.00000000 + Condition 1.00000000 3.11480166 1.18583253 1.00000000 4.00000000 1.00000000 + Edge Ratio 1.00006177 4.80707901 1.51313656 1.00000000 4.00000000 1.00000000 + Jacobian 0.00011326 0.28172540 0.10292251 0.00000000 999.99900000 1.00000000 + Minimum Angle 29.30873612 89.99827738 73.08079323 40.00000000 90.00000000 90.00000000 + Maximum Angle 90.00132792 156.87642432 109.37004979 90.00000000 135.00000000 90.00000000 + Area Sign 1.00000000 1.00000000 1.00000000 1.00000000 1.00000000 1.00000000 +``` +The call to `generate_mesh` also prints mesh quality statistics to the screen and updates the +visualization. The background grid is *removed* from the visualization when the mesh is generated. + +!!! note "Mesh visualization" + Currently, only the "skeleton" of the mesh is visualized. Thus, the high-order curved boundary information + is not seen in the plot but this information **is present** in the generated mesh file. + +![initial_blob](https://user-images.githubusercontent.com/25242486/174747052-d0776ca6-5451-4d9f-accb-8d97b2db1c26.png) + +Inspecting the mesh we see that the automatic subdivision in HOHQMesh does well to capture the fine features +of the curved outer boundary. Although, we see that the mesh near the point $(-4, 0)$ is still quite coarse. +To remedy this we manually add a `RefinementCenter` near this region of the domain to force HOHQMesh to increase +the resolution in this area. We create and add this refinement region to the current project with +```julia +center = newRefinementCenter("region", "smooth", [-4.0, -0.5, 0.0], 0.4, 1.0) +addRefinementRegion!(blob_project, center) +``` +Above we create a circular refinement region centered at the point $(-4, -0.5)$ with a desired resolution size +$0.4$ and a radius of $1.0$. Upon adding this refinement region to `blob_project`, the visualization will +update to indicate the location and size of the manual refinement region. + +![refinement_blob](https://user-images.githubusercontent.com/25242486/174747059-1f58ae14-aeec-48d5-afb3-6a0614a8e29d.png) + +## Final mesh + +With the refinement region added to the project we can regenerate the mesh. Note, this will create +and save new output files `TheBlob.control`, `TheBlob.tec`, `TheBlob.mesh` and update the figure. +```julia +generate_mesh(blob_project) + + ******************* + 2D Mesh Statistics: + ******************* + Total time = 0.11373499999999999 + Number of nodes = 505 + Number of Edges = 940 + Number of Elements = 438 + Number of Subdivisions = 5 + + Mesh Quality: + Measure Minimum Maximum Average Acceptable Low Acceptable High Reference + Signed Area 0.00025346 0.36181966 0.11412658 0.00000000 999.99900000 1.00000000 + Aspect Ratio 1.00002884 2.47585614 1.26424390 1.00000000 999.99900000 1.00000000 + Condition 1.00000000 3.11480166 1.18425870 1.00000000 4.00000000 1.00000000 + Edge Ratio 1.00006177 4.80707901 1.51413601 1.00000000 4.00000000 1.00000000 + Jacobian 0.00011326 0.28172540 0.09814547 0.00000000 999.99900000 1.00000000 + Minimum Angle 29.30873612 89.99827901 72.79596483 40.00000000 90.00000000 90.00000000 + Maximum Angle 90.00132782 156.87642433 109.63912468 90.00000000 135.00000000 90.00000000 + Area Sign 1.00000000 1.00000000 1.00000000 1.00000000 1.00000000 1.00000000 +``` +Note, the circular region indicating the refinement center is removed from the plot when the mesh is generated. + +![final_blob](https://user-images.githubusercontent.com/25242486/174747066-a804bf1d-508a-480d-bde3-47687b402604.png) + +Now we decide that we are satisfied with the mesh quality and resolution of the outer boundary curve. + +## Summary + +In this tutorial we demonstrated how to: +* Define a curved outer boundary using parametric equations. +* Add and adjust the background grid. +* Visualize an interactive mesh project. +* Add manual refinement to a local region of the domain. + +For completeness, we include a script with all the commands to generate the mesh displayed in the final image. +Note, we **do not** include the plotting in this script. +```julia +# Interactive mesh with a curved outer boundary +# +# Create an outer boundary from a set of parametric equations. +# Add manual refinement in a small region around the point (-4, -0.5). +# +# Keywords: outer boundary, parametric equations, refinement center +using HOHQMesh + +# Instantiate the project +blob_project = newProject("TheBlob", "out") + +# Create and add the outer boundary curve +xEqn = "x(t) = 4 * cos(2 * pi * t) - 0.6 * cos(8 * pi * t)^3" +yEqn = "y(t) = 4 * sin(2 * pi * t) - 0.5 * sin(11* pi * t)^2" +zEqn = "z(t) = 0.0" +blob = newParametricEquationCurve("Blob", xEqn, yEqn, zEqn) +addCurveToOuterBoundary!(blob_project, blob) + +# Add the background grid +addBackgroundGrid!(blob_project, [0.5, 0.5, 0.0]) + +# Create and add the refinement region +center = newRefinementCenter("region", "smooth", [-4.0, -0.5, 0.0], 0.4, 1.0) +addRefinementRegion!(blob_project, center) + +# Generate the mesh +generate_mesh(blob_project) +``` \ No newline at end of file diff --git a/docs/src/tutorials/introduction.md b/docs/src/tutorials/introduction.md new file mode 100644 index 00000000..fddd42b6 --- /dev/null +++ b/docs/src/tutorials/introduction.md @@ -0,0 +1,80 @@ +# [Tutorials for HOHQMesh.jl](@id Tutorials) + +The tutorial section for [HOHQMesh.jl](https://github.com/trixi-framework/HOHQMesh.jl) +provides step-by-step commands and accompanying explanations for the major features of the +[interactive mesh generation tools](@ref InteractiveTool). + +For a general overview of the capabilities and features of HOHQMesh to generate quadrilateral +and hexahedral meshes we refer to the +[Pre-made Examples](https://trixi-framework.github.io/HOHQMesh/examples/) of the HOHQMesh +documentation. + +For more information on how an unstructured mesh generated with HOHQMesh.jl can be used in +the simulation framework [Trixi.jl](https://github.com/trixi-framework/Trixi.jl) see the +[relevant tutorial](https://trixi-framework.github.io/Trixi.jl/stable/tutorials/hohqmesh_tutorial/). + +## [Straight-sided outer boundary](@ref) + +This tutorial gives an introduction to the main functionality of the interactive meshing. In +particular, adding a straight-sided bounding box for the outer domain and two circular inner boundary +chains. It also demonstrates how to adjust some of the mesh parameters as well as the output mesh file +format. + +### Synopsis + +Demonstrates how to: +* Query and adjust the `RunParameters` of a project. +* Define a rectangular outer boundary and set the background grid. +* Visualize an interactive mesh project. +* Add circular inner boundary curves. + +## [Curved outer boundary](@ref) + +This tutorial constructs an outer domain boundary using parametric equations. The background grid is then +set and a preliminary mesh is generated. It highlights how a user can manually add a refinement region where +necessary from this visual inspection. + +### Synopsis + +Demonstrates how to: +* Define a curved outer boundary using parametric equations. +* Add and adjust the background grid. +* Visualize an interactive mesh project. +* Add manual refinement to a local region of the domain. + +## [Spline curves](@ref) + +This tutorial constructs a circular outer domain and three inner boundary curves. Two of the inner curves +are constructed using cubic splines and the third inner boundary is a triangular shape built from +three straight line "curves". + +### Synopsis + +Demonstrates how to: +* Create a circular outer boundary curve. +* Add the background grid when an outer boundary curve is present. +* Visualize an interactive mesh project. +* Construct and add parametric spline curves. +* Construct and add an inner boundary chain of straight line segments. + +## [Creating and editing curves](@ref) + +This tutorial demonstrates how to construct and edit curve segments defined in inner / outer boundary +chains. A curve "chain" in the HOHQMesh context means a closed curve that is +composed of an arbitrary number of pieces. +Each curve segment of a chain can be a different curve type, e.g., a circular +arc can connect to a spline that connects to a parametric equation curve. +There are details for the removal and replacement of a portion of a chain. + +### Synopsis + +Demonstrates how to: +* Create and edit an outer boundary chain. +* Create and edit an inner boundary chain. +* Add the background grid when an outer boundary curve is present. +* Visualize an interactive mesh project. +* Discuss undo / redo capabilities. +* Construct and add parametric spline curves. +* Construct and add a curve from parametric equations. +* Construct and add straight line segments. +* Construct and add circular arc segments. \ No newline at end of file diff --git a/docs/src/tutorials/spline_curves.md b/docs/src/tutorials/spline_curves.md new file mode 100644 index 00000000..7f68b941 --- /dev/null +++ b/docs/src/tutorials/spline_curves.md @@ -0,0 +1,254 @@ +# Spline curves + +The purpose of this tutorial is to demonstrate how to create an unstructured mesh on +a domain with a curved outer boundary and three inner boundaries. +Two of the inner curves are built from cubic splines. The third inner curve is a +triangular shape built from a chain of three straight line "curves". +The outer boundary, inner boundaries, background grid and mesh +will be visualized for quality inspection. + +It provides details and clarification for the script `interactive_spline_curves.jl` +from the [examples](https://github.com/trixi-framework/HOHQMesh.jl/tree/main/examples) folder. + +### Synopsis + +This tutorial demonstrates how to: +* Create a circular outer boundary curve. +* Add the background grid when an outer boundary curve is present. +* Visualize an interactive mesh project. +* Construct and add parametric spline curves. +* Construct and add an inner boundary chain of straight line segments. + +## Initialization + +From a Julia REPL we load the HOHQMesh package as well as +[GLMakie](https://github.com/JuliaPlots/GLMakie.jl/), a backend of +[Makie.jl](https://github.com/JuliaPlots/Makie.jl/), to visualize the +curves, mesh, etc. from the interactive tool. +```julia +julia> using GLMakie, HOHQMesh +``` +Now we are ready to interactively generate unstructured quadrilateral meshes! + +We create a new project with the name `"spline_curves"` and +assign `"out"` to be the folder where any output files from the mesh generation process +will be saved. By default, the output files created by HOHQMesh will carry the same name +as the project. For example, the resulting HOHQMesh control file from this tutorial +will be named `spline_curves.control`. +If the folder `out` does not exist, it will be created automatically in +the current file path. +```julia +spline_project = newProject("spline_curves", "out") +``` + +## Add the outer boundary + +The outer boundary curve for this tutorial is a circle of radius $r=4$ centered at +the point $(0, -1)$. +We define this circular curve with the function `newCircularArcCurve` as follows +```julia +circ = newCircularArcCurve("outerCircle", # curve name + [0.0, -1.0, 0.0], # circle center + 4.0, # circle radius + 0.0, # start angle + 360.0, # end angle + "degrees") # angle units +``` +We use `"degrees"` to set the angle bounds, but `"radians"` can also be used. +The name of the curve stored in the dictionary `circ` is assigned to be `"outerCircle"`. +This curve name is also the label that HOHQMesh will give to this boundary curve in the +resulting mesh file. + +The new `circ` curve is then added to the `spline_project` as an outer boundary curve with +```julia +addCurveToOuterBoundary!(spline_project, circ) +``` + +## Add a background grid + +HOHQMesh requires a background grid for the mesh generation process. This background grid sets +the base resolution of the desired mesh. HOHQMesh will automatically subdivide from this background +grid near sharp features of any curved boundaries. + +For a domain bounded by an outer boundary curve, this background grid is set by indicating the desired +element size in the $x$ and $y$ directions. To start, we set the background grid for `spline_project` +to have elements with side length $0.6$ in each direction +```julia +addBackgroundGrid!(spline_project, [0.6, 0.6, 0.0]) +``` + +We next visualize the outer boundary curve and background grid with the following +```julia +plotProject!(spline_project, MODEL+GRID) +``` +Here, we take the sum of the keywords `MODEL` and `GRID` in order to simultaneously visualize +the outer boundary and background grid. The resulting plot is given below. The chain of outer boundary +curves is called `"Outer"` and it contains a single curve `"outerCircle"` labeled in the figure by `O.1`. + +![background_grid](https://user-images.githubusercontent.com/25242486/174798948-3a00c5b5-d910-45df-9a9a-088eb3fa360a.png) + +## Add the inner boundaries + +The domain of this tutorial will contain three inner boundary curves: +1. Cubic spline curve created from data points read in from a file. +2. Cubic spline curve created from points directly given in the code. +3. Triangular shape built from three straight line "curves". + +### Cubic spline with data from a file + +A parametric cubic spline curve can be constructed from a file of data points. The first line +of this plain text file must indicate the number of nodes. Then line-by-line the file contains +the knots $t_j$, $x_j$, $y_j$, $z_j$ where $j$ indexes the number of nodes. +If the spline curve is to be closed. The last data point must be the same as the first. +For examples, see the +[HOHQMesh documentation](https://trixi-framework.github.io/HOHQMesh/the-model/#the-spline-curve-definition) +or open the file `test_spline_curve_data.txt` in the +[examples](https://github.com/trixi-framework/HOHQMesh.jl/tree/main/examples) folder + +We create a parametric spline curve from a file with +```julia +spline1 = newSplineCurve("big_spline", joinpath(@__DIR__, "examples", "test_spline_curve_data.txt")) +``` +The name of the curve stored in the dictionary `spline1` is assigned to be `"big_spline"`. +This curve name is also the label that HOHQMesh will give to this boundary curve in the +resulting mesh file. + +The new `spline1` curve is then added to the `spline_project` as an inner boundary curve with +```julia +addCurveToInnerBoundary!(spline_project, spline1, "inner1") +``` +This inner boundary chain name `"inner1"` is used internally by HOHQMesh. The visualization +of the background grid automatically detects that a curve has been added to the project +and the plot is updated appropriately, as shown below. The chain for the inner boundary +curve is called `inner1` and it contains a single curve `"big_spline"` labeled in the figure by `1.1`. + +![one_curve](https://user-images.githubusercontent.com/25242486/174798958-5e4a57b3-ece0-4d11-a004-b39909fccbad.png) + +### Cubic spline from data in Julia + +Alternatively, a parametric cubic spline curve can be constructed directly from data points +provided in the code. These points take the form `[t, x, y, z]` where `t` is the parameter variable +that varies between $0$ and $1$. For the spline construction, the number of points is included as an +input argument as well as the actual parametric point data. +Again, if the spline curve is to be closed, the first and last data point **must** match. + +Below, we construct another parametric spline using this strategy that consists of five data points +```julia +spline_data = [ [0.0 1.75 -1.0 0.0] + [0.25 2.1 -0.5 0.0] + [0.5 2.7 -1.0 0.0] + [0.75 0.6 -2.0 0.0] + [1.0 1.75 -1.0 0.0] ] + +spline2 = newSplineCurve("small_spline", 5, spline_data) +``` +The name of the curve stored in the dictionary `spline2` is assigned to be `"small_spline"`. +This curve name is also the label that HOHQMesh will give to this boundary curve in the +resulting mesh file. + +The new `spline2` curve is then added to the `spline_project` as an inner boundary curve with +```julia +addCurveToInnerBoundary!(spline_project, spline2, "inner2") +``` +This inner boundary chain name `"inner2"` is used internally by HOHQMesh. The visualization +of the background grid automatically detects that a curve has been added to the project +and the plot is updated appropriately, as shown below. The chain for the inner boundary +curve is called `inner2` and it contains a single curve `"small_spline"` labeled in the figure by `2.1`. + +![two_curves](https://user-images.githubusercontent.com/25242486/174798962-99e0673d-e0f9-444a-a71d-7dfd9412306e.png) + +### Triangular shape + +Finally, we build a triangular shaped inner boundary curve built from a chain of three +straight lines. Each line segment is defined using the function `newEndPointsLineCurve`. +We construct the three line segments that define the edges of a triangular shape with +```julia +edge1 = newEndPointsLineCurve("triangle", # curve name + [-2.3, -1.0, 0.0], # start point + [-1.7, -1.0, 0.0]) # end point + +edge2 = newEndPointsLineCurve("triangle", # curve name + [-1.7, -1.0, 0.0], # start point + [-2.0, -0.4, 0.0]) # end point + +edge3 = newEndPointsLineCurve("triangle", # curve name + [-2.0, -0.4, 0.0], # start point + [-2.3, -1.0, 0.0]) # end point +``` +Here, each edge of the curve is given the same name `"triangle"` as this curve name +is also the label that HOHQMesh will give to this boundary curve in the +resulting mesh file. + +The three line segments `edge1`, `edge2`, and `edge3` are connected in a +counter-clockwise orientation as required by HOHQMesh. +```julia +addCurveToInnerBoundary!(spline_project, edge1, "inner3") +addCurveToInnerBoundary!(spline_project, edge2, "inner3") +addCurveToInnerBoundary!(spline_project, edge3, "inner3") +``` +The inner boundary chain name `"inner3"` is used internally for HOHQMesh. Again, +the active visualization automatically detects that new curves have been added to the project +and the plot is updated appropriately, as shown below. The chain for the inner triangular boundary +is called `inner3` and it contains a three curve segments all called `"triangle"` labeled in the figure +by `3.1`, `3.2`, and `3.3`. + +![three_curves](https://user-images.githubusercontent.com/25242486/174798968-d41d7d8e-db1e-466f-a4fa-c36f14dfae98.png) + +## Generate the mesh + +With the background grid, outer boundary curve, and all inner boundary curves added to the `spline_project` we are ready to generate the mesh. +This will output the following files to the `out` folder: + +* `spline_curves.control`: A HOHQMesh control file for the current project. +* `spline_curves.tec`: A TecPlot formatted file to visualize the mesh with other software, e.g., [ParaView](https://www.paraview.org/). +* `spline_curves.mesh`: A mesh file with format `ISM-V2` (the default format). + +To do this we execute the command +```julia +generate_mesh(spline_project) + 1 chevron elements removed from mesh. + 1 chevron elements removed from mesh. + + ******************* + 2D Mesh Statistics: + ******************* + Total time = 0.29613000000000000 + Number of nodes = 1177 + Number of Edges = 2225 + Number of Elements = 1047 + Number of Subdivisions = 4 + + Mesh Quality: + Measure Minimum Maximum Average Acceptable Low Acceptable High Reference + Signed Area 0.00006214 0.15607014 0.04505181 0.00000000 999.99900000 1.00000000 + Aspect Ratio 1.00008989 2.78073390 1.23192911 1.00000000 999.99900000 1.00000000 + Condition 1.00000055 3.81350981 1.15526066 1.00000000 4.00000000 1.00000000 + Edge Ratio 1.00014319 6.76310951 1.46264239 1.00000000 4.00000000 1.00000000 + Jacobian 0.00001495 0.10424741 0.03955903 0.00000000 999.99900000 1.00000000 + Minimum Angle 37.25504203 89.96195708 74.41060580 40.00000000 90.00000000 90.00000000 + Maximum Angle 90.03105286 157.27881545 107.90994073 90.00000000 135.00000000 90.00000000 + Area Sign 1.00000000 1.00000000 1.00000000 1.00000000 1.00000000 1.00000000 +``` +The call to `generate_mesh` also prints mesh quality statistics to the screen. +HOHQMesh also reports mesh clean-up that occurred during the generation process, in this case the removal of +"bad" chevron shaped elements that were present within the automatic subdivision procedure. +The visualization updates automatically and the background grid is *removed* after when the mesh is generated. + +!!! note "Mesh visualization" + Currently, only the "skeleton" of the mesh is visualized. Thus, the high-order curved boundary information + is not seen in the plot but this information **is present** in the generated mesh file. + +![final_spline](https://user-images.githubusercontent.com/25242486/174798986-6b900fa8-840c-4c04-bc61-00f0749af1be.png) + +Inspecting the mesh we see that the automatic subdivision in HOHQMesh does well to capture the fine features +of the curved inner boundaries, particularly near the sharp angles of the `"big_spline"` curve. We decide that we +are satisfied with the overall mesh quality. + +## Summary + +In this tutorial we demonstrated how to: +* Create a circular outer boundary curve. +* Add the background grid when an outer boundary curve is present. +* Visualize an interactive mesh project. +* Construct and add parametric spline curves. +* Construct and add an inner boundary chain of straight line segments. \ No newline at end of file diff --git a/docs/src/tutorials/straight_outer_boundary.md b/docs/src/tutorials/straight_outer_boundary.md new file mode 100644 index 00000000..7ec39262 --- /dev/null +++ b/docs/src/tutorials/straight_outer_boundary.md @@ -0,0 +1,206 @@ +# Straight-sided outer boundary + +The purpose of this tutorial is to demonstrate how to create an unstructured mesh on +a rectangular domain that contains two circular inner boundaries. Further, we show how +to adjust some of the default mesh parameters as well as the +output mesh file format. The outer boundary, background grid and mesh +will be visualized for quality inspection. + +It provides details and clarification for the script `interactive_outer_box_two_circles.jl` +from the [examples](https://github.com/trixi-framework/HOHQMesh.jl/tree/main/examples) folder. + +### Synopsis + +This tutorial demonstrates how to: +* Query and adjust the `RunParameters` of a project. +* Define a rectangular outer boundary and set the background grid. +* Visualize an interactive mesh project. +* Add circular inner boundary curves. + +## Initialization + +From a Julia REPL we load the HOHQMesh package as well as +[GLMakie](https://github.com/JuliaPlots/GLMakie.jl/), a backend of +[Makie.jl](https://github.com/JuliaPlots/Makie.jl/), to visualize the +curves, mesh, etc. from the interactive tool. +```julia +julia> using GLMakie, HOHQMesh +``` +Now we are ready to interactively generate unstructured quadrilateral meshes! + +We create a new project with the name `"box_two_circles"` and +assign `"out"` to be the folder where any output files from the mesh generation process +will be saved. By default, the output files created by HOHQMesh will carry the same name +as the project. For example, the resulting HOHQMesh control file from this tutorial +will be named `box_two_circles.control`. +If the folder `out` does not exist, it will be created automatically in +the current file path. +```julia +box_project = newProject("box_two_circles", "out") +``` + +## Adjusting project parameters + +When a new project is created it is filled with several default +`RunParameters` such as the polynomial order used to represent curved boundaries +or the mesh file format. These `RunParameters` can be queried and adjusted with +appropriate getter/setter pairs, see [Controlling the mesh generation](@ref) +for more details. + +For the `box_project` we first query the current values for the polynomial +order and the mesh output format +```julia +julia> getPolynomialOrder(box_project) +5 + +julia> getMeshFileFormat(box_project) +"ISM-V2" +``` + +We change these quantities in the `box_project` with the corresponding +setter functions. For this we will set the polynomial order to be $4$ and the mesh file format +to be `ABAQUS`. See the +[P4est-based mesh](https://trixi-framework.github.io/Trixi.jl/stable/meshes/p4est_mesh/) +section of the [Trixi.jl](https://github.com/trixi-framework/Trixi.jl) documentation for a +detailed overview of this mesh file format. +```julia +setPolynomialOrder!(box_project, 4) +setMeshFileFormat!(box_project, "ABAQUS") +``` + +## Add the background grid + +HOHQMesh requires a background grid for the mesh generation process. This background grid sets +the base resolution of the desired mesh. HOHQMesh will automatically subdivide from this background +grid near any curved boundaries. + +The domain for this tutorial is a rectangular box with the bounds $[0,30]\times[0,15]$. Because no +outer boundary curve is present there are two (equivalent) strategies for us to define the bounds +of a rectangular domain and the size of the background grid: +1. Set the lower left corner point of the domain $(x_0, y_0)$, define the element size in each spatial direction + $\Delta x$ and $\Delta y$, and the number of steps taken in each direction $N_x, N_y$. The resulting background + grid will have the extent $[x_0, x_0 + N_x \Delta x]$ by $[y_0, y_0 + N_y \Delta y]$. For this example, we set + a background grid of Cartesian elements with size one in each dimension with + the following commands + ```julia + lower_left = [0.0, 0.0, 0.0] + spacing = [1.0, 1.0, 0.0] + num_intervals = [30, 15, 0] + addBackgroundGrid!(box_project, lower_left, spacing, num_intervals) + ``` +2. Set the bounding box with extent values ordered as `[top, left, bottom, right]` and provide the number + the number of steps in each direction. To set a background grid of Cartesian elements with size one in + each dimension for the rectangular box $[0,30]\times[0,15]$ we use + ```julia + bounds = [15.0, 0.0, 0.0, 30.0] + N = [30, 15, 0] + addBackgroundGrid!(box_project, bounds, N) + ``` +Next, we visualize the `box_project` to ensure that the background grid has been added correctly. +```julia +plotProject!(box_project, GRID) +``` +We use the keyword and `GRID` to indicate that we want the background grid to be included in the +visualization. + +![background](https://user-images.githubusercontent.com/25242486/174775018-86936c6b-ba69-456e-9aaf-c5054a4aacbe.png) + +## Add the inner boundaries + +Next, we add the two circular inner boundary curves with different radii. + +The first circle will have radius $r=2$ and be centered at the point $(4, 4)$. +We define this circular curve with the function `newCircularArcCurve` as follows +```julia +circle1 = newCircularArcCurve("circle1", # curve name + [4.0, 4.0, 0.0], # circle center + 2.0, # circle radius + 0.0, # start angle + 360.0, # end angle + "degrees") # angle units +``` +We use `"degrees"` to set the angle bounds, but `"radians"` can also be used. +The name of the curve stored in the dictionary `circle1` is assigned to be `"circle1"`. +This curve name is also the label that HOHQMesh will give to this boundary curve in the +resulting mesh file. + +The new `circle1` curve is then added to the `box_project` as an inner boundary curve with +```julia +addCurveToInnerBoundary!(box_project, circle1, "inner1") +``` +This inner boundary chain name `"inner1"` is used internally by HOHQMesh. The visualization +of the background grid automatically detects that a curve has been added to the project +and the plot is updated appropriately, as shown below. The chain for the inner boundary +curve is called `"inner1"` and it contains a single curve `"circle1"` labeled in the figure by `1.1`. + +![first_circle](https://user-images.githubusercontent.com/25242486/174775027-62a094f7-bbba-4c1c-a389-99562c2e5fe2.png) + +With analogous steps we create another circular curve with radius $r=4$, centered at $(20, 9)$ and +add it as a second inner curve to the `box_project`. Note, for this curve we use radians +for the angle units. +```julia +circle2 = newCircularArcCurve("circle2", # curve name + [20.0, 9.0, 0.0], # circle center + 4.0, # circle radius + 0.0, # start angle + 2.0 * pi, # end angle + "radians") # angle units +addCurveToInnerBoundary!(box_project, circle2, "inner2") +``` +Again, the `box_project` detects that a curve has been added to it +and the visualization is automatically updated with the second circular curve. +The chain for the second inner boundary curve is called `"inner2"` and it contains +a single curve `"circle2"` labeled in the figure by `2.1`. + +![second_circle](https://user-images.githubusercontent.com/25242486/174775037-a9144f93-78da-48ae-976b-7cfaeca68240.png) + +## Generate the mesh + +With the background grid and all inner boundary curves added to the `box_project` +we can generate the mesh. +This will output the following files to the `out` folder: + +* `box_two_circles.control`: A HOHQMesh control file for the current project. +* `box_two_circles.tec`: A TecPlot formatted file to visualize the mesh with other software, e.g., [ParaView](https://www.paraview.org/). +* `box_two_circles.inp`: A mesh file with format `ABAQUS` that was set above. + +To do this we execute the command +```julia +generate_mesh(box_project) + + ******************* + 2D Mesh Statistics: + ******************* + Total time = 3.5553999999999995E-002 + Number of nodes = 498 + Number of Edges = 921 + Number of Elements = 422 + Number of Subdivisions = 0 + + Mesh Quality: + Measure Minimum Maximum Average Acceptable Low Acceptable High Reference + Signed Area 0.34513058 1.15383206 0.91833833 0.00000000 999.99900000 1.00000000 + Aspect Ratio 1.00000004 1.71083844 1.08733476 1.00000000 999.99900000 1.00000000 + Condition 1.00000000 1.46558793 1.04922640 1.00000000 4.00000000 1.00000000 + Edge Ratio 1.00000006 2.43503343 1.16700825 1.00000000 4.00000000 1.00000000 + Jacobian 0.17863168 1.07210721 0.86801359 0.00000000 999.99900000 1.00000000 + Minimum Angle 50.56155029 89.99999787 83.84466557 40.00000000 90.00000000 90.00000000 + Maximum Angle 90.00000259 136.97479459 96.69930735 90.00000000 135.00000000 90.00000000 + Area Sign 1.00000000 1.00000000 1.00000000 1.00000000 1.00000000 1.00000000 +``` +The call to `generate_mesh` also prints mesh quality statistics to the screen and updates the +visualization. The background grid is *removed* from the visualization when the mesh is generated and the resulting +mesh is visualized instead. + +![final_circle](https://user-images.githubusercontent.com/25242486/174775040-e4a04503-83f3-4f80-b087-972bd8dbb5e9.png) + +From a visual inspection we decide that we are satisfied with the mesh quality and resolution near +the inner circular boundaries. + +## Summary + +In this tutorial we demonstrated how to: +* Query and adjust the `RunParameters` of a project. +* Define a rectangular outer boundary and set the background grid. +* Visualize an interactive mesh project. +* Add circular inner boundary curves. diff --git a/examples/AllFeatures.control b/examples/AllFeatures.control new file mode 100644 index 00000000..dd44f8a8 --- /dev/null +++ b/examples/AllFeatures.control @@ -0,0 +1,116 @@ +% +% Control file that exercises available 2D features +% + +\begin{MODEL} + \begin{OUTER_BOUNDARY} + \begin{END_POINTS_LINE} + name = B1 + xEnd = [20.0,-5,0.0] + xStart = [-20.0,-5.0,0.0] + \end{END_POINTS_LINE} + \begin{END_POINTS_LINE} + name = B2 + xEnd = [0.0,25.28,0.0] + xStart = [20.0,-5.0,0.0] + \end{END_POINTS_LINE} + \begin{END_POINTS_LINE} + name = B3 + xEnd = [-20.0,-5.0,0.0] + xStart = [0.0,25.28,0.0] + \end{END_POINTS_LINE} + \end{OUTER_BOUNDARY} + \begin{INNER_BOUNDARIES} + \begin{CHAIN} + name = Arc + \begin{CIRCULAR_ARC} + units = degrees + name = InnerCircle1 + radius = 1.0 + start angle = 0.0 + center = [-12.0,-1.5,0.0] + end angle = 360.0 + \end{CIRCULAR_ARC} + \end{CHAIN} + \begin{CHAIN} + name = InnerSpline + \begin{SPLINE_CURVE} + name = Spline + nKnots = 26 + \begin{SPLINE_DATA} + 0.0 -3.5 3.5 0.0 + 0.03846153846153846 -3.2 5.0 0.0 + 0.07692307692307693 -2.0 6.0 0.0 + 0.115384615384615 1.0 6.0 0.0 + 0.153846153846154 2.0 5.0 0.0 + 0.192307692307692 3.0 4.0 0.0 + 0.230769230769231 5.0 4.0 0.0 + 0.269230769230769 6.0 5.0 0.0 + 0.307692307692308 7.0 7.0 0.0 + 0.346153846153846 8.0 8.0 0.0 + 0.384615384615385 9.0 8.0 0.0 + 0.423076923076923 10.0 7.0 0.0 + 0.461538461538462 11.0 5.0 0.0 + 0.5 11.0 3.0 0.0 + 0.538461538461539 10.0 2.0 0.0 + 0.576923076923077 9.0 1.0 0.0 + 0.615384615384615 7.0 1.0 0.0 + 0.653846153846154 5.0 1.0 0.0 + 0.692307692307692 3.0 1.0 0.0 + 0.730769230769231 1.0 0.0 0.0 + 0.769230769230769 0.0 -1.0 0.0 + 0.807692307692308 -1.0 -1.0 0.0 + 0.846153846153846 -2.0 -0.8 0.0 + 0.884615384615385 -2.5 0.0 0.0 + 0.923076923076923 -3.0 1.0 0.0 + 1.0 -3.5 3.5 0.0 + \end{SPLINE_DATA} + \end{SPLINE_CURVE} + \end{CHAIN} + \begin{CHAIN} + name = InnerCircle2 + \begin{PARAMETRIC_EQUATION_CURVE} + name = Circle1 + yEqn = f(t) = 17.0 + 1.5*sin(2*pi*t) + zEqn = z(t) = 0.0 + xEqn = f(t) = 1.5*cos(2*pi*t) + \end{PARAMETRIC_EQUATION_CURVE} + \end{CHAIN} + \end{INNER_BOUNDARIES} +\end{MODEL} +\begin{CONTROL_INPUT} + \begin{REFINEMENT_REGIONS} + \begin{REFINEMENT_CENTER} + name = center + w = 0.5 + x0 = [9.0,-3.0,0.0] + type = smooth + h = 0.1 + \end{REFINEMENT_CENTER} + \begin{REFINEMENT_LINE} + name = line + x1 = [2.0,14.0,0.0] + w = 0.5 + x0 = [-6.0,9.0,0.0] + type = smooth + h = 0.2 + \end{REFINEMENT_LINE} + \end{REFINEMENT_REGIONS} + \begin{SPRING_SMOOTHER} + smoothing type = LinearAndCrossbarSpring + smoothing = ON + number of iterations = 25 + \end{SPRING_SMOOTHER} + \begin{BACKGROUND_GRID} + background grid size = [3.0,3.0,0.0] + \end{BACKGROUND_GRID} + \begin{RUN_PARAMETERS} + mesh file name = examples/AllFeatures.mesh + plot file format = skeleton + plot file name = examples/AllFeatures.tec + stats file name = none + mesh file format = ISM-V2 + polynomial order = 4 + \end{RUN_PARAMETERS} +\end{CONTROL_INPUT} +\end{FILE} diff --git a/examples/HalfCircle3DRot.control b/examples/HalfCircle3DRot.control new file mode 100644 index 00000000..5036ed5b --- /dev/null +++ b/examples/HalfCircle3DRot.control @@ -0,0 +1,49 @@ +\begin{CONTROL_INPUT} + + \begin{RUN_PARAMETERS} + mesh file name = examples/HalfCircle3DR.inp + plot file name = examples/HalfCircle3DR.tec + stats file name = none + mesh file format = ABAQUS + polynomial order = 5 + plot file format = sem + \end{RUN_PARAMETERS} + + \begin{BACKGROUND_GRID} + background grid size = [0.75,0.75,0.75] + \end{BACKGROUND_GRID} + + \begin{SIMPLE_ROTATION} + direction = 1 + rotation angle factor = 1.0 + subdivisions = 6 + start surface name = start + end surface name = end + \end{SIMPLE_ROTATION} + + \begin{SPRING_SMOOTHER} + smoothing = ON + smoothing type = LinearAndCrossBarSpring + number of iterations = 15 + \end{SPRING_SMOOTHER} + +\end{CONTROL_INPUT} + +\begin{MODEL} + + \begin{OUTER_BOUNDARY} + \begin{PARAMETRIC_EQUATION_CURVE} + name = circle + xEqn = x(t) = 4.0*cos(pi*t) + yEqn = y(t) = 0.5 + 4.0*sin(pi*t) + zEqn = z(t) = 0.0 + \end{PARAMETRIC_EQUATION_CURVE} + \begin{END_POINTS_LINE} + name = cut + xStart = [-4.0,0.5,0.0] + xEnd = [4.0,0.5,0.0] + \end{END_POINTS_LINE} + \end{OUTER_BOUNDARY} + +\end{MODEL} +\end{FILE} diff --git a/examples/NACA0012.control b/examples/NACA0012.control index a8155a9d..cea3a477 100644 --- a/examples/NACA0012.control +++ b/examples/NACA0012.control @@ -24,6 +24,7 @@ \begin{REFINEMENT_REGIONS} \begin{REFINEMENT_CENTER} + name = TECenter type = smooth x0 = [1.0,0.0,0.0] h = 0.10 @@ -31,6 +32,7 @@ \end{REFINEMENT_CENTER} \begin{REFINEMENT_CENTER} + name = LECenter type = smooth x0 = [0.0,0.0,0.0] h = 0.05 diff --git a/examples/interactive_from_control_file.jl b/examples/interactive_from_control_file.jl new file mode 100644 index 00000000..c20718cc --- /dev/null +++ b/examples/interactive_from_control_file.jl @@ -0,0 +1,39 @@ +# Interactive mesh from a HOHQMesh control file +# +# Reads in the `AllFeatures.control` file, creates a `HQMTool` project, +# and generates a mesh file. More details abuot the outer / inner boundary +# curves, refinement regions, etc. of HOHQMesh can be found in its documentation +# https://trixi-framework.github.io/HOHQMesh/ +# +# Keywords: outer boundary chain, inner boundary chain, refinement region, control file read in + +using HOHQMesh + +# Set the file path of the control file to be read in for this example + +all_features_control_file = joinpath( HOHQMesh.examples_dir() , "AllFeatures.control" ) + +# Read in the HOHQMesh control file and create the project dictionary that stores +# the different components of a mesh, i.e., boundary curves, refinement regions, etc. +# as well as set the output folder where any generated files will be saved. + +p = openProject(all_features_control_file, "out") + +# Plot the project model curves and background grid + +if isdefined(Main, :Makie) + plotProject!(p, MODEL+GRID) + @info "Press enter to generate the mesh and update the plot." + readline() + else # Throw an informational message about plotting to the user + @info "To visualize the project (boundary curves, background grid, mesh, etc.), include `GLMakie` and run again." + end + +# Generate the mesh. This produces the mesh and TecPlot files `AllFeatures.mesh` and `AllFeatures.tec` +# and save them to the `out` folder. Also, if there is an active plot in the project `p` it is +# updated with the mesh that was generated. + +generate_mesh(p) + +# After the mesh successfully generates mesh statistics, such as the number of corner nodes, +# the number of elements etc., are printed to the REPL. \ No newline at end of file diff --git a/examples/interactive_outer_boundary.jl b/examples/interactive_outer_boundary.jl new file mode 100644 index 00000000..8b732f28 --- /dev/null +++ b/examples/interactive_outer_boundary.jl @@ -0,0 +1,69 @@ +# Interactive mesh with an outer boundary constructed by a user +# +# Create a circular outer boundary and an inner ice cream cone shaped boundary +# chain consisting of three curves, lay a background grid and generate a HOHQMesh +# directly from the project object. +# +# Keywords: outer boundary chain, inner boundary chain + +using HOHQMesh + +# Create a new project with the name "IceCreamCone", which will also be the +# name of the mesh, plot and stats files, written to output folder `out`. + +p = newProject("IceCreamCone", "out") + +# Outer boundary for this example mesh is a complete circle. Add it into the project. + +circ = newCircularArcCurve("outerCircle", [0.0, -1.0, 0.0], 4.0, 0.0, 360.0, "degrees") +addCurveToOuterBoundary!(p, circ) + +# Inner boundary is three curves. Two straight lines and a circular arc. +# Note the three curve are connected to ensure a counter-clockwise orientation +# as required by HOHQMesh + +# Create the three interior curves. The individual names of each curve in the inner +# chain are used internally by HOHQMesh and are output as the given boundary names in +# the mesh file. + +cone1 = newEndPointsLineCurve("cone1", [0.0, -3.0, 0.0], [1.0, 0.0, 0.0]) +iceCream = newCircularArcCurve("iceCream", [0.0, 0.0, 0.0], 1.0, 0.0, 180.0, "degrees") +cone2 = newEndPointsLineCurve("cone2", [-1.0, 0.0, 0.0], [0.0, -3.0, 0.0]) + +# Assemble the three curve in a closed chain oriented couter-clockwise. The chain +# name `IceCreamCone` is only used internally by HOHQMesh. + +addCurveToInnerBoundary!(p, cone1, "IceCreamCone") +addCurveToInnerBoundary!(p, iceCream, "IceCreamCone") +addCurveToInnerBoundary!(p, cone2, "IceCreamCone") + +# Adjust some `RunParameters` and overwrite the defaults values. In this case, we +# set a new value for the boundary order polynomial representation and adjust the +# output plot file format to be `sem` + +setPolynomialOrder!(p, 4) +setPlotFileFormat!(p, "sem") + +# A background grid is required for the mesh generation. In this example we lay a +# background grid of Cartesian boxes with size 0.5. + +addBackgroundGrid!(p, [0.5, 0.5, 0.0]) + +# Plot the project model curves and background grid + +if isdefined(Main, :Makie) + plotProject!(p, MODEL+GRID) + @info "Press enter to generate the mesh and update the plot." + readline() + else # Throw an informational message about plotting to the user + @info "To visualize the project (boundary curves, background grid, mesh, etc.), include `GLMakie` and run again." + end + +# Generate the mesh. This produces the mesh and TecPlot files `IceCreamCone.mesh` and `IceCreamCone.tec` +# and saves them to the `out` folder. Also, if there is an active plot in the project `p` it is +# updated with the mesh that was generated. + +generate_mesh(p) + +# After the mesh successfully generates mesh statistics, such as the number of corner nodes, +# the number of elements etc., are printed to the REPL. \ No newline at end of file diff --git a/examples/interactive_outer_boundary_generic.jl b/examples/interactive_outer_boundary_generic.jl new file mode 100644 index 00000000..ef55a8e0 --- /dev/null +++ b/examples/interactive_outer_boundary_generic.jl @@ -0,0 +1,67 @@ +# Interactive mesh with an outer boundary constructed by a user +# using generic function calls +# +# Create a circular outer boundary and an inner ice cream cone shaped boundary +# chain consisting of three curves, lay a background grid and generate a HOHQMesh +# directly from the project object. +# +# In particular, this example highligths available generic functionality for creating +# and adding new curves to a project. +# +# Keywords: outer boundary chain, inner boundary chain, generic functions + +using HOHQMesh + +# Create a new project with the name "IceCreamCone", which will also be the +# name of the mesh, plot and stats files, written to output folder `out`. + +p = newProject("IceCreamCone", "out") + +# Outer boundary for this example mesh is a complete circle. Add it into the project. + +circ = new("outerCircle", [0.0, -1.0, 0.0], 4.0, 0.0, 2.0 * pi, "radians") +add!(p, circ) + +# Inner boundary is three curves. Two straight lines and a circular arc. +# Note the three curve are connected to ensure a counter-clockwise orientation +# as required by HOHQMesh + +# Create the three interior curves. The individual names of each curve in the inner +# chain are used internally by HOHQMesh and are output as the given boundary names in +# the mesh file. + +cone1 = new("cone1", [0.0,-3.0,0.0], [1.0,0.0,0.0]) +iceCream = new("iceCream", [0.0,0.0,0.0], 1.0, 0.0, 180.0, "degrees") +cone2 = new("cone2", [-1.0,0.0,0.0], [0.0,-3.0,0.0]) + +# Assemble the three curve in a closed chain oriented couter-clockwise. The chain +# name `IceCreamCone` is only used internally by HOHQMesh. + +add!(p, cone1, "IceCreamCone") +add!(p, iceCream, "IceCreamCone") +add!(p, cone2, "IceCreamCone") + +# A background grid is required for the mesh generation. In this example we lay a +# background grid of Cartesian boxes with size 0.5. + +addBackgroundGrid!(p, [0.5,0.5,0.0]) + +# To plot the project model curves and the background grid, type `using GLMakie` +# Plot the project model curves and background grid + +if isdefined(Main, :Makie) + plotProject!(p, MODEL+GRID) + @info "Press enter to generate the mesh and update the plot." + readline() + else # Throw an informational message about plotting to the user + @info "To visualize the project (boundary curves, background grid, mesh, etc.), include `GLMakie` and run again." + end + +# Generate the mesh. This produces the mesh and TecPlot files `AllFeatures.mesh` and `AllFeatures.tec` +# and save them to the `out` folder. Also, if there is an active plot in the project `p` it is +# updated with the mesh that was generated. + +generate_mesh(p) + +# After the mesh successfully generates mesh statistics, such as the number of corner nodes, +# the number of elements etc., are printed to the REPL. \ No newline at end of file diff --git a/examples/interactive_outer_box_two_circles.jl b/examples/interactive_outer_box_two_circles.jl new file mode 100644 index 00000000..589226e6 --- /dev/null +++ b/examples/interactive_outer_box_two_circles.jl @@ -0,0 +1,74 @@ +# Interactive mesh with a rectangular outer boundary and two circular inner boundaries +# +# Create a domain with two circular inner boundaries and a rectangular outer boundary +# that is straight-sided. Set the output mesh file format to be ABAQUS. +# +# Keywords: Straight-sided outer boundary, inner boundary chain, ABAQUS file format + +using HOHQMesh + +# Create a new project with the name "box_two_circles", which will also be the +# name of the mesh, plot and stats files, written to output folder `out`. + +p = newProject("box_two_circles", "out") + +# Adjust some `RunParameters` and overwrite the defaults values. In this case, we +# set a new value for the boundary order polynomial representation and adjust the +# output mesh file format to be `ABAQUS`, which will produce a mesh file +# `box_two_circles.inp` + +setPolynomialOrder!(p, 4) +setMeshFileFormat!(p, "ABAQUS") + +# Outer boundary for this example mesh wil be a rectangular box. For this the user +# can set the lower left most point of the box, the spacing size in each coordinate +# direction `[Δx, Δy, Δz]`, and the number of intervals taken. + +lower_left = [0.0, 0.0, 0.0] +spacing = [1.0, 1.0, 0.0] +num_intervals = [30, 15, 0] + +# These three quantities set the background grid that is required by HOHQMesh for a given domain. + +addBackgroundGrid!(p, lower_left, spacing, num_intervals) + +# Inner boundaries for this example will be two circles with different radii. + +# A circle with radius 2.0, centered at [4.0, 4.0, 0.0]. Note, we use degrees to set the angle. +# This inner boundary curve name will be written to the mesh file. + +circle1 = newCircularArcCurve("circle1", [4.0, 4.0, 0.0], 2.0, 0.0, 360.0, "degrees") + +# Add `circle1` into the project as an inner boundary +# This chian name in only used internally by HOHQMesh. + +addCurveToInnerBoundary!(p, circle1, "inner1") + +# A circle with radius 4.0, centered at [20.0, 9.0, 0.0]. Note, the user can use radians to set the angle. +# This inner boundary curve name will be written to the mesh file. + +circle2 = newCircularArcCurve("circle2", [20.0, 9.0, 0.0], 4.0, 0.0, 2.0 * pi, "radians") + +# Add `circle2` into the project as an inner boundary +# This chian name in only used internally by HOHQMesh. + +addCurveToInnerBoundary!(p, circle2, "inner2") + +# Plot the project model curves and background grid + +if isdefined(Main, :Makie) + plotProject!(p, MODEL+GRID) + @info "Press enter to generate the mesh and update the plot." + readline() +else # Throw an informational message about plotting to the user + @info "To visualize the project (boundary curves, background grid, mesh, etc.), include `GLMakie` and run again." +end + +# Generate the mesh. This produces the mesh and TecPlot files `AllFeatures.mesh` and `AllFeatures.tec` +# and save them to the `out` folder. Also, if there is an active plot in the project `p` it is +# updated with the mesh that was generated. + +generate_mesh(p) + +# After the mesh successfully generates mesh statistics, such as the number of corner nodes, +# the number of elements etc., are printed to the REPL. \ No newline at end of file diff --git a/examples/interactive_spline_curves.jl b/examples/interactive_spline_curves.jl new file mode 100644 index 00000000..eabe2886 --- /dev/null +++ b/examples/interactive_spline_curves.jl @@ -0,0 +1,87 @@ +# Interactive mesh with spline curves +# +# Create a mesh with a circular outer boundary and three inner boundaries. +# Two inner boundaries are parametric splines, on econstructred from file data and +# the other from given data points. The third curve is a triangular object built from +# three internal straight-sided "curves". +# +# Keywords: spline from file, spline construction, outer boundary, inner boundary + +projectName = "spline_boundary" +projectPath = "out" + +# Create a new project with the name "spline_boundary", which will also be the +# name of the mesh, plot and stats files, written to output folder `out`. + +p = newProject(projectName, projectPath) + +# A background grid is required for the mesh generation. In this example we lay a +# background grid of Cartesian boxes with size 0.6 in each direction. + +addBackgroundGrid!(p, [0.6, 0.6, 0.0]) + +# Outer boundary for this example mesh is a complete circle. Add it into the project. + +circ = newCircularArcCurve("outerCircle", [0.0, -1.0, 0.0], 4.0, 0.0, 360.0, "degrees") +addCurveToOuterBoundary!(p, circ) + +# Inner boundaries will have three curves: +# (i) Cubic spline curve created from data points read in from a file. +# (ii) Cubic spline curve created from points directly given in the code. +# (iii) Triangle shape built from three straight line "curves". + +# Create the three interior curves. The curve name are those that are output as the +# given boundary names in the mesh file. + +# First inner boundary is a parametric cubic spline read in from a file. For information +# on formatting cubic spline data files see the HOHQMesh documentation: +# https://trixi-framework.github.io/HOHQMesh/the-model/#the-spline-curve-definition + +spline1 = newSplineCurve("big_spline", joinpath(@__DIR__, "test_spline_curve_data.txt")) +addCurveToInnerBoundary!(p, spline1, "inner1") + +# Second inner boundary is a parametric cubic spline with data points directly provided +# in the code. These points take the form [t, x, y, z] where `t` is the parameter variable. +# For the spline construction the number of points is included as an input argument as well as +# the actual parametric point data. + +spline_data = [ [0.0 1.75 -1.0 0.0] + [0.25 2.1 -0.5 0.0] + [0.5 2.7 -1.0 0.0] + [0.75 0.6 -2.0 0.0] + [1.0 1.75 -1.0 0.0] ] + +spline2 = newSplineCurve("small_spline", 5, spline_data) +addCurveToInnerBoundary!(p, spline2, "inner2") + +# Third inner boundary is a triangular shape built from three straight lines "curves". +# The three lines are connected in a counter-clockwise orientation as required by HOHQMesh. +# Note that we give the three inner curves the same name "triangle" that will be the +# boundary name given in the mesh file. The inner boundary chain name `inner3` is used +# internally for HOHQMesh but is not known to the mesh file naming. + +edge1 = newEndPointsLineCurve("triangle", [-2.3, -1.0, 0.0], [-1.7, -1.0, 0.0]) +edge2 = newEndPointsLineCurve("triangle", [-1.7, -1.0, 0.0], [-2.0, -0.4, 0.0]) +edge3 = newEndPointsLineCurve("triangle", [-2.0, -0.4, 0.0], [-2.3, -1.0, 0.0]) +addCurveToInnerBoundary!(p, edge1, "inner3") +addCurveToInnerBoundary!(p, edge2, "inner3") +addCurveToInnerBoundary!(p, edge3, "inner3") + +# Plot the project model curves and background grid + +if isdefined(Main, :Makie) + plotProject!(p, MODEL+GRID) + @info "Press enter to generate the mesh and update the plot." + readline() + else # Throw an informational message about plotting to the user + @info "To visualize the project (boundary curves, background grid, mesh, etc.), include `GLMakie` and run again." + end + +# Generate the mesh. This produces the mesh and TecPlot files `IceCreamCone.mesh` and `IceCreamCone.tec` +# and saves them to the `out` folder. Also, if there is an active plot in the project `p` it is +# updated with the mesh that was generated. + +generate_mesh(p) + +# After the mesh successfully generates mesh statistics, such as the number of corner nodes, +# the number of elements etc., are printed to the REPL. \ No newline at end of file diff --git a/examples/test_spline_curve_data.txt b/examples/test_spline_curve_data.txt new file mode 100644 index 00000000..2e576e1d --- /dev/null +++ b/examples/test_spline_curve_data.txt @@ -0,0 +1,78 @@ +77 +0.000000000000000 -1.000000000000000 -1.000000000000000 0.000000000000000 +0.013157894736842 -0.995929487847041 -0.984513308895507 0.000000000000000 +0.026315789473684 -0.984006935621602 -0.940263574448587 0.000000000000000 +0.039473684210526 -0.964665819673838 -0.870566304959074 0.000000000000000 +0.052631578947368 -0.938339616353904 -0.778737008726803 0.000000000000000 +0.065789473684211 -0.905461802011955 -0.668091194051611 0.000000000000000 +0.078947368421053 -0.866465852998146 -0.541944369233333 0.000000000000000 +0.092105263157895 -0.821785245662633 -0.403612042571803 0.000000000000000 +0.105263157894737 -0.771853456355570 -0.256409722366859 0.000000000000000 +0.118421052631579 -0.717103961427114 -0.103652916918335 0.000000000000000 +0.131578947368421 -0.657970237227418 0.051342865473934 0.000000000000000 +0.144736842105263 -0.594885760106638 0.205262116510112 0.000000000000000 +0.157894736842105 -0.528284006414929 0.354789327890363 0.000000000000000 +0.171052631578947 -0.458598452502447 0.496608991314852 0.000000000000000 +0.184210526315789 -0.386262574719347 0.627405598483744 0.000000000000000 +0.197368421052632 -0.311709849415783 0.743863641097203 0.000000000000000 +0.210526315789474 -0.235373752941912 0.842667610855393 0.000000000000000 +0.223684210526316 -0.157687761647887 0.920501999458480 0.000000000000000 +0.236842105263158 -0.079085351883865 0.974051298606627 0.000000000000000 +0.250000000000000 0.000000000000000 1.000000000000000 0.000000000000000 +0.263157894736842 0.079132214191990 0.996076583425322 0.000000000000000 +0.276315789473684 0.157864797034136 0.964185481015558 0.000000000000000 +0.289473684210526 0.235748651406911 0.907275112990232 0.000000000000000 +0.302631578947368 0.312334680190782 0.828293899568867 0.000000000000000 +0.315789473684211 0.387173786266220 0.730190260970987 0.000000000000000 +0.328947368421053 0.459816872513694 0.615912617416116 0.000000000000000 +0.342105263157895 0.529814841813676 0.488409389123779 0.000000000000000 +0.355263157894737 0.596718597046633 0.350628996313498 0.000000000000000 +0.368421052631579 0.660079041093037 0.205519859204799 0.000000000000000 +0.381578947368421 0.719447076833358 0.056030398017204 0.000000000000000 +0.394736842105263 0.774373607148064 -0.094890967029763 0.000000000000000 +0.407894736842105 0.824409534917626 -0.244295815716577 0.000000000000000 +0.421052631578947 0.869105763022515 -0.389235727823714 0.000000000000000 +0.434210526315789 0.908013194343199 -0.526762283131652 0.000000000000000 +0.447368421052632 0.940682731760148 -0.653927061420865 0.000000000000000 +0.460526315789474 0.966665278153833 -0.767781642471830 0.000000000000000 +0.473684210526316 0.985511736404724 -0.865377606065024 0.000000000000000 +0.486842105263158 0.996773009393289 -0.943766531980922 0.000000000000000 +0.500000000000000 1.000000000000000 -1.000000000000000 0.000000000000000 +0.513157894736842 0.994898517068294 -1.032013465103201 0.000000000000000 +0.526315789473684 0.981793993293483 -1.041277883073334 0.000000000000000 +0.539473684210526 0.961166767333847 -1.030148084893675 0.000000000000000 +0.552631578947368 0.933497177847667 -1.000978901547498 0.000000000000000 +0.565789473684211 0.899265563493221 -0.956125164018079 0.000000000000000 +0.578947368421053 0.858952262928790 -0.897941703288692 0.000000000000000 +0.592105263157895 0.813037614812655 -0.828783350342616 0.000000000000000 +0.605263157894737 0.762001957803095 -0.751004936163122 0.000000000000000 +0.618421052631579 0.706325630558390 -0.666961291733489 0.000000000000000 +0.631578947368421 0.646488971736821 -0.579007248036990 0.000000000000000 +0.644736842105263 0.582972319996667 -0.489497636056901 0.000000000000000 +0.657894736842105 0.516256013996209 -0.400787286776498 0.000000000000000 +0.671052631578947 0.446820392393727 -0.315231031179056 0.000000000000000 +0.684210526315789 0.375145793847500 -0.235183700247849 0.000000000000000 +0.697368421052632 0.301712557015808 -0.163000124966155 0.000000000000000 +0.710526315789474 0.227001020556933 -0.101035136317247 0.000000000000000 +0.723684210526316 0.151491523129152 -0.051643565284402 0.000000000000000 +0.736842105263158 0.075664403390749 -0.017180242850895 0.000000000000000 +0.750000000000000 0.000000000000000 0.000000000000000 0.000000000000000 +0.763157894736842 -0.075055193385125 -0.001715681169683 0.000000000000000 +0.776315789473684 -0.149190063107908 -0.020972184616666 0.000000000000000 +0.789473684210526 -0.222127340511945 -0.055672422052361 0.000000000000000 +0.802631578947368 -0.293589756940828 -0.103719305188178 0.000000000000000 +0.815789473684211 -0.363300043738154 -0.163015745735530 0.000000000000000 +0.828947368421053 -0.430980932247516 -0.231464655405828 0.000000000000000 +0.842105263157895 -0.496355153812509 -0.306968945910482 0.000000000000000 +0.855263157894737 -0.559145439776727 -0.387431528960906 0.000000000000000 +0.868421052631579 -0.619074521483765 -0.470755316268511 0.000000000000000 +0.881578947368421 -0.675865130277217 -0.554843219544706 0.000000000000000 +0.894736842105263 -0.729239997500677 -0.637598150500906 0.000000000000000 +0.907894736842105 -0.778921854497740 -0.716923020848520 0.000000000000000 +0.921052631578947 -0.824633432612001 -0.790720742298961 0.000000000000000 +0.934210526315789 -0.866097463187054 -0.856894226563639 0.000000000000000 +0.947368421052632 -0.903036677566493 -0.913346385353967 0.000000000000000 +0.960526315789474 -0.935173807093912 -0.957980130381355 0.000000000000000 +0.973684210526316 -0.962231583112907 -0.988698373357216 0.000000000000000 +0.986842105263158 -0.983932736967071 -1.003404025992960 0.000000000000000 +1.000000000000000 -1.000000000000000 -1.000000000000000 0.000000000000000 diff --git a/src/ControlFile/ControlFileOperations.jl b/src/ControlFile/ControlFileOperations.jl new file mode 100644 index 00000000..8827143d --- /dev/null +++ b/src/ControlFile/ControlFileOperations.jl @@ -0,0 +1,383 @@ +#= + ImportControlFile(fileName::String) + +The control file reader parses the control file and returns a Control file dictionary. + +@author: davidkopriva + +A Control file dictionary contains the keys + TYPE + CONTROL_INPUT + MODEL + +TYPE is a string naming the type (class) of object stored + +The CONTROL_INPUT contains the blocks + RUN_PARAMETERS + MESH_PARAMETERS + SPRING_SMOOTHER + REFINEMENT_REGIONS + REFINEMENT_REGIONS contains a ["LIST"] of + REFINEMENT_CENTER + REFINEMENT_LINE + SCALE_TRANSFORMATION + ROTATION_TRANSFORMATION + SIMPLE_EXTRUSION + SIMPLE_ROTATION + SWEEP_ALONG_CURVE + +The MODEL dictionary contains the keys + TYPE + OUTER_BOUNDARY + OUTER_BOUNDARY contains a ["LIST"] of + PARAMETRIC_EQUATION_CURVE + SPLINE_CURVE + END_POINTS_LINE + CIRCULAR_ARC + INNER_BOUNDARIES + The INNER_BOUNDARIES block contains a ["LIST"] of + CHAIN + SWEEP_CURVE + SWEEP_SCALE_FACTOR + TOPOGRAPHY + +A CHAIN block contains a ["LIST"] of + PARAMETRIC_EQUATION_CURVE + SPLINE_CURVE + END_POINTS_LINE + CIRCULAR_ARC + +A PARAMETRIC_EQUATION_CURVE dictionary contains the keys + TYPE + name + xEqn + yEqn + zEqn + +A SPLINE_CURVE block contains the keys + TYPE + name + SPLINE_DATA + +SPLINE_DATA block contains keys and data + nKnots + t_1 x_1 y_1 z_1 + t_2 x_2 y_2 z_2 + ... + t_nKnots x_nKnots y_nKnots z_nKnots + +An END_POINTS_LINE has the following keys + TYPE + name + xStart + xEnd + +A CIRCULAR_ARC block contains + TYPE + name + units + center + radius + start angle + end angle + +REFINEMENT_REGIONS dictionary contains the keys + TYPE + LIST + LIST is a list of + REFINEMENT_CENTER + REFINEMENT_LINE + +A REFINEMENT_CENTER contains the keys + TYPE + center + h + w + +A REFINEMENT_LINE contains the keys + TYPE + xStart + xEnd + h + w + +A ROTATION_TRANSFORMATION contains the keys + TYPE + direction + rotationPoint + +A SCALE_TRANSFORMATION contains the keys + TYPE + origin + scaleFactor + +The SWEEP_CURVE dictionary contains the keys + TYPE + LIST + The list contains dictionaries describing + PARAMETRIC_EQUATION_CURVE + SPLINE_CURVE + END_POINTS_LINE + +The SWEEP_SCALE_FACTOR dictionary contains the keys + TYPE + LIST + The list contains dictionaries describing + PARAMETRIC_EQUATION + + But the equation definitions contain only one equation r(t) = ... + +The TOPOGRAPHY dictionary contains the keys + TYPE + eqn (for equation defined topography) + sizing + +The SIMPLE_EXTRUSION block contains the keys + TYPE + direction + height + subdivisions + start surface name + end surface name + +The SIMPLE_ROTATION block contains the keys + TYPE + direction + rotation angle factor + subdivisions + start surface name + end surface name + +The SWEEP_ALONG_CURVE block contains the keys + TYPE + algorithm (optional) + subdivisions per segment + start surface name + end surface name + +=# + +# Four objects store their members as lists rather than +# as dictionaries (See above) + +const blocksThatStoreLists = Set(["OUTER_BOUNDARY", + "REFINEMENT_REGIONS" , + "INNER_BOUNDARIES", + "CHAIN"]) + +const blockRegex = r"(?<=\{).+?(?=\})" +blockNameStack = [] + +# +#--------------- MAIN ENTRY ----------------------------------------------- +# +function ImportControlFile(fileName::String) + controlDict = Dict{String,Any}() + open(fileName,"r") do controlFile + performImport(controlDict, controlFile) + end + return controlDict +end + + +function WriteControlFile(controlDict::Dict{String,Any}, fileName::String) + open(fileName,"w") do controlFile + indent = "" + WriteDictionary(controlDict, controlFile, indent) + println(controlFile,"\\end{FILE}") + end +end +# +#------------- END MAIN ENTRY ------------------------------------------ +# + +function performImport(collection, f::IOStream) + + for line in eachline(f) + line = rstrip(line) +# +# ---------------- +# Start of a block +# ---------------- +# + if occursin("begin{",line) + blockNameMatch = match(blockRegex,line) + if blockNameMatch === nothing + error("Block name not found in string: " * line) + else # Start new collection + blockName = blockNameMatch.match + push!(blockNameStack, blockName) +# +# A SPLINE_DATA block is special and is read in separately +# into an array and saved in the spline curve dictionary +# + if blockName == "SPLINE_DATA" + ImportSplineData( collection, f) + continue + end + + newBlock = Dict{String,Any}() + newBlock["TYPE"] = blockName + addToCollection(collection, blockName, newBlock) +# +# Some blocks store items in a list +# + if in(blockName, blocksThatStoreLists) + newBlock["LIST"] = Dict{String,Any}[] +# +# If the block defines a chain, get its name +# + if blockName == "CHAIN" + nextLine = readline(f) + kvp = keyAndValueOnLine(nextLine) + if kvp === nothing + error("Key-value pair not found in string: " * nextLine) + end + addToCollection(newBlock,kvp[1],kvp[2]) + end + performImport(newBlock["LIST"],f) + else + performImport(newBlock,f) + end + end +# +# -------------- +# End of a block +# -------------- +# + elseif occursin("end{",line) + blockNameMatch = match(blockRegex,line) + blockName = blockNameMatch.match + if blockName == "FILE" + return + end + if length(blockNameStack) == 0 + error("Extra end statement found: " * line) + end + if blockNameMatch === nothing + error("Block name not found in string: " * line) + else + stackValue::String = blockNameStack[end] + if cmp(blockName,stackValue) == 0 + pop!(blockNameStack) + else + error("Block name end $blockName does not match current block $stackValue") + end + if blockName == "SPLINE_DATA" + continue + else + return + end + end +# +# ---------------------- +# Comment or blank lines +# ---------------------- +# + elseif isempty(line) + continue + elseif line[1] == '%' + continue +# +# ------------------------- +# Block body key-value pair +# ------------------------- +# + else + kvp = keyAndValueOnLine(line) + if kvp === nothing + error("Key-value pair not found in string: " * line) + end + addToCollection(collection,kvp[1],kvp[2]) + end + end +end + + +function WriteDictionary(controlDict::Dict{String,Any}, f::IOStream, indent::String) + + deepIndent = " " * indent + for (key, value) in controlDict + if isa(value, AbstractDict) + println(f,indent,"\\begin{$key}") + if in(key,blocksThatStoreLists) + list = value["LIST"] + StepThroughList(list,f, deepIndent) + else + WriteDictionary(value,f, deepIndent) + end + println(f,indent,"\\end{$key}") + elseif isa(value, AbstractString) + if key != "TYPE" + println(f,indent,"$key = $value") + end + elseif isa(value, AbstractArray) + if key == "LIST" + StepThroughList(value,f, deepIndent) + elseif key == "SPLINE_DATA" + println(f,indent,"\\begin{$key}") + arraySize = size(value) + for j = 1:arraySize[1] + println(f,deepIndent, " ", value[j,1], " ", value[j,2], " ", value[j,3], " ", value[j,4]) + end + println(f,indent,"\\end{$key}") + end + end + end +end + + +function StepThroughList(lst::AbstractArray,f::IOStream, indent::String) + deepIndent = " " * indent + for dict in lst + dtype = dict["TYPE"] + println(f,indent, "\\begin{$dtype}") + WriteDictionary(dict,f, deepIndent) + println(f,indent, "\\end{$dtype}") + end +end + + +function keyAndValueOnLine(s) + indxOfEqual = findfirst("=",s) + if indxOfEqual === nothing + return nothing + end + key = strip(s[1:indxOfEqual.start-1],[' ','\t']) + value = strip(s[indxOfEqual.stop+1:end],[' ','\t']) + return (key,value) +end + + +function addToCollection(dict::Dict{String,Any}, k::AbstractString, v::AbstractString) + dict[k] = v +end + + +function addToCollection(c::Array, k::AbstractString, v::Any) + push!(c,v) +end + + +function addToCollection(dict::Dict{String,Any}, k::AbstractString, v::Dict{String,Any}) + dict[k] = v +end + + +function ImportSplineData( splineDict::Dict{String,Any}, f::IOStream) + + if !haskey(splineDict, "nKnots") + error("Spline block must define nKnots before SPLINE_DATA. Try again.") + end + + knotString = splineDict["nKnots"] + nKnots = parse(Int64, knotString) + splineDataArray = zeros(Float64, nKnots, 4) + for i = 1:nKnots + currentLine = split(readline(f)) + for j = 1:4 + splineDataArray[i,j] = parse(Float64, currentLine[j]) + end + end + splineDict["SPLINE_DATA"] = splineDataArray +end diff --git a/src/Curves/CurveOperations.jl b/src/Curves/CurveOperations.jl new file mode 100644 index 00000000..c55aa623 --- /dev/null +++ b/src/Curves/CurveOperations.jl @@ -0,0 +1,222 @@ + +const argRegex = r"(?<=\().+?(?=\))" + +function arcCurvePoints(center::Array{Float64}, r::Float64, thetaStart::Float64, thetaEnd::Float64, units::AbstractString, t::Array{Float64}, points::Array{Float64,2}) + fctr::Float64 = 1.0 + if units == "degrees" + fctr = pi/180.0 + end + + theta::Float64 = 0.0 + for i = 1:length(t) + theta = thetaStart + (thetaEnd - thetaStart)*t[i] + points[i,1] = center[1] + r*cos(theta*fctr) + points[i,2] = center[2] + r*sin(theta*fctr) + end +end + + +function arcCurvePoint(center::Array{Float64}, r::Float64, thetaStart::Float64, thetaEnd::Float64, + units::AbstractString, t::Float64, point::Array{Float64}) + fctr::Float64 = 1.0 + if units == "degrees" + fctr = pi/180.0 + end + theta = thetaStart + (thetaEnd - thetaStart)*t + point[1] = center[1] + r*cos(theta*fctr) + point[2] = center[2] + r*sin(theta*fctr) +end + + +function endPointsLineCurvePoints(xStart::Array{Float64}, xEnd::Array{Float64}, t::Array{Float64}, points::Array{Float64}) + for i = 1:length(t) + points[i,1:2] = xStart[1:2] + t[i]*(xEnd[1:2] - xStart[1:2]) + end +end + + +function endPointsLineCurvePoint(xStart::Array{Float64}, xEnd::Array{Float64}, t::Float64, point::Array{Float64}) + point[1:2] = xStart[1:2] + t*(xEnd[1:2] - xStart[1:2]) +end + + +function peEquationCurvePoints(xEqn, yEqn, t::Array{Float64}, points::Array{Float64,2}) + + argPart,eqString = keyAndValueFromString(xEqn) + xArgM = match(argRegex,argPart) + xArg = Symbol(xArgM.match) + ex = Meta.parse(eqString) + + argPart,eqString = keyAndValueFromString(yEqn) + yArgM = match(argRegex,argPart) + yArg = Symbol(yArgM.match) + ey = Meta.parse(eqString) + + for i = 1:length(t) + points[i,1] = evalWithDict(ex,Dict(xArg=> t[i])) + points[i,2] = evalWithDict(ey,Dict(yArg=> t[i])) + end +end + + +function peEquationCurvePoint(xEqn, yEqn, t::Float64, point::Array{Float64}) + + argPart,eqString = keyAndValueFromString(xEqn) + xArgM = match(argRegex,argPart) + xArg = Symbol(xArgM.match) + ex = Meta.parse(eqString) + + argPart,eqString = keyAndValueFromString(yEqn) + yArgM = match(argRegex,argPart) + yArg = Symbol(yArgM.match) + ey = Meta.parse(eqString) + + point[1] = evalWithDict(ex, Dict(xArg=> t)) + point[2] = evalWithDict(ey, Dict(yArg=> t)) + +end + + +function splineCurvePoints(nKnots::Int, splineData::Array{Float64,2}, points::Array{Float64,2}) + + xSpline = constructSpline(nKnots,splineData[:,1],splineData[:,2]) + ySpline = constructSpline(nKnots,splineData[:,1],splineData[:,3]) + + sz = size(points) + nPts = sz[1] + t = 0.0 + for i = 1:nPts + t = (i-1)/(nPts-1) + points[i,1] = evalSpline(xSpline,t) + points[i,2] = evalSpline(ySpline,t) + end +end + + +function splineCurvePoint(nKnots::Int, splineData::Array{Float64,2}, t, point::Array{Float64}) + + xSpline = constructSpline(nKnots,splineData[:,1],splineData[:,2]) + ySpline = constructSpline(nKnots,splineData[:,1],splineData[:,3]) + + point[1] = evalSpline(xSpline,t) + point[2] = evalSpline(ySpline,t) +end + + +# This function evaluates a string as an equation, might be redundant code +# function parse_eval_dict(s::AbstractString, locals::Dict{Symbol}) +# ex = Meta.parse(s) +# assignments = [:($sym = $val) for (sym,val) in locals] +# eval(:(let $(assignments...); $ex; end)) +# end + + +function evalWithDict(ex, locals::Dict{Symbol}) + assignments = [:($sym = $val) for (sym,val) in locals] + eval(:(let $(assignments...); $ex; end)) +end + + +function curvePoints(crvDict::Dict{String,Any}, N::Int) +# N = Number of intervals + curveType::String = crvDict["TYPE"] + + if curveType == "PARAMETRIC_EQUATION_CURVE" + xEqn = crvDict["xEqn"] + yEqn = crvDict["yEqn"] + + x = zeros(Float64,N+1,2) + t = zeros(Float64,N+1) + for i = 1:N+1 + t[i] = (i-1)/N + end + peEquationCurvePoints(xEqn,yEqn,t,x) + elseif curveType == "END_POINTS_LINE" + xStart = realArrayForKeyFromDictionary("xStart",crvDict) + xEnd = realArrayForKeyFromDictionary("xEnd",crvDict) + x = zeros(Float64,3,2) + t = zeros(Float64,3) + for i = 1:3 + t[i] = (i-1)/2.0 + end + + endPointsLineCurvePoints(xStart,xEnd,t,x) + elseif curveType == "CIRCULAR_ARC" + center = realArrayForKeyFromDictionary("center",crvDict) + radius = realForKeyFromDictionary("radius",crvDict) + startAngle = realForKeyFromDictionary("start angle",crvDict) + endAngle = realForKeyFromDictionary("end angle",crvDict) + units = crvDict["units"] + + x = zeros(Float64,N+1,2) + t = zeros(Float64,N+1) + for i = 1:N+1 + t[i] = (i-1)/N + end + + arcCurvePoints(center,radius,startAngle,endAngle,units,t,x) + elseif curveType == "SPLINE_CURVE" + nKnots = intForKeyFromDictionary("nKnots",crvDict) + splineData = crvDict["SPLINE_DATA"] + + M = max(N,nKnots*2) + x = zeros(Float64,M+1,2) + + splineCurvePoints(nKnots,splineData,x) + end + return x +end + + +function chainPoints(chain::Array{Dict{String,Any}}, N::Int) + + x = Any[] + + for crvDict in chain + push!(x,curvePoints(crvDict,N)) + end + return x +end + + +function curvePoint(crvDict::Dict{String,Any}, t::Float64) + + curveType::String = crvDict["TYPE"] + + if curveType == "PARAMETRIC_EQUATION_CURVE" + xEqn = crvDict["xEqn"] + yEqn = crvDict["yEqn"] + x = zeros(Float64,3) + peEquationCurvePoint(xEqn, yEqn, t, x) + elseif curveType == "END_POINTS_LINE" + xStart = realArrayForKeyFromDictionary("xStart",crvDict) + xEnd = realArrayForKeyFromDictionary("xEnd",crvDict) + x = zeros(Float64,3) + endPointsLineCurvePoint(xStart,xEnd,t,x) + elseif curveType == "CIRCULAR_ARC" + center = realArrayForKeyFromDictionary("center",crvDict) + radius = realForKeyFromDictionary("radius",crvDict) + startAngle = realForKeyFromDictionary("start angle",crvDict) + endAngle = realForKeyFromDictionary("end angle",crvDict) + units = crvDict["units"] + x = zeros(Float64,3) + arcCurvePoint(center,radius,startAngle,endAngle,units,t,x) + elseif curveType == "SPLINE_CURVE" + nKnots = intForKeyFromDictionary("nKnots",crvDict) + splineData = crvDict["SPLINE_DATA"] + x = zeros(Float64,3) + splineCurvePoint(nKnots,splineData,t,x) + end + return x +end + + +function curvesMeet(firstCurve::Dict{String,Any}, secondCurve::Dict{String,Any}; tol=100*eps(Float64)) + xFirst = curvePoint(firstCurve,1.0) + xSecond = curvePoint(secondCurve,0.0) + if maximum(abs.(xFirst - xSecond)) < tol + return true + else + return false + end +end diff --git a/src/Curves/Spline.jl b/src/Curves/Spline.jl new file mode 100644 index 00000000..d93d8501 --- /dev/null +++ b/src/Curves/Spline.jl @@ -0,0 +1,108 @@ + +mutable struct Spline + N::Int + x::Array{Float64} + y::Array{Float64} + b::Array{Float64} + c::Array{Float64} + d::Array{Float64} + last::Int +end + +function constructSpline(N::Int, x::Array{Float64},y::Array{Float64}) + b = zeros(Float64,N) + c = zeros(Float64,N) + d = zeros(Float64,N) + + Nm1 = N - 1 +# +# Set up tri-diagonal system for a cubic spline +# + d[1] = x[2] - x[1] + c[2] = (y[2] - y[1])/d[1] + for i = 2:Nm1 + d[i] = x[i+1] - x[i] + b[i] = 2.0*(d[i-1] + d[i]) + c[i+1] = (y[i+1] - y[i])/d[i] + c[i] = c[i+1] - c[i] + end +# +# "not-a-knot" end conditions where third derivatives are approximated +# with divided differences +# + b[1] = -d[1] + b[N] = -d[N-1] + c[1] = c[3]/(x[4] - x[2]) - c[2]/(x[3] - x[1]) + c[N] = c[N-1]/(x[N] - x[N-2]) - c[N-2]/(x[N-1] - x[N-3]) + c[1] = c[1]*d[1]^2/(x[4] - x[1]) + c[N] = -c[N]*d[N-1]^2/(x[N] - x[N-3]) +# +# Forward elimination +# + t = 0.0 + for i = 2:N + t = d[i-1]/b[i-1] + b[i] = b[i] - t*d[i-1] + c[i] = c[i] - t*c[i-1] + end +# +# Back substitution +# + c[N] = c[N]/b[N] + for ib = 1:Nm1 + i = N - ib + c[i] = (c[i] - d[i]*c[i+1])/b[i] + end +# +# Compute polynomial coefficients +# + b[N] = (y[N] - y[Nm1])/d[Nm1] + d[Nm1]*(c[Nm1] + 2.0*c[N]) + for i = 1: Nm1 + b[i] = (y[i+1] - y[i])/d[i] - d[i]*(c[i+1] + 2.0*c[i]) + d[i] = (c[i+1] - c[i])/d[i] + c[i] = 3.0*c[i] + end + c[N] = 3.0*c[N] + d[N] = d[N-1] + + spl = Spline(N,x,y,b,c,d,1) + return spl +end + + +function evalSpline(spl::Spline, u::Float64) + N = spl.N + s = 0.0 + i = spl.last + + if i >= N + i = 1 + end + + if spl.x[i] < u <= spl.x[i+1] + dx = u - spl.x[i] + s = spl.y[i] + dx*(spl.b[i]+ dx*(spl.c[i] + dx*spl.d[i])) + spl.last = i + return s + end + + i = 1 + j = N+1 + + for ii = 1:N + k = div(i+j,2) + if u < spl.x[k] + j = k + end + if u >= spl.x[k] + i = k + end + if j <= i+1 + break + end + end + dx = u - spl.x[i] + s = spl.y[i] + dx*(spl.b[i]+ dx*(spl.c[i] + dx*spl.d[i])) + spl.last = i + return s +end diff --git a/src/HOHQMesh.jl b/src/HOHQMesh.jl index 244baa25..58678810 100644 --- a/src/HOHQMesh.jl +++ b/src/HOHQMesh.jl @@ -1,9 +1,151 @@ module HOHQMesh -import HOHQMesh_jll +# Include other packages that are used in HOHQMesh +# (standard library packages first, other packages next, all of them sorted alphabetically) +using HOHQMesh_jll: HOHQMesh_jll +using Requires: @require + +function __init__() + # Enable features that depend on the availability of the Makie package + @require Makie="ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" begin + using .Makie + include("Viz/VizProject.jl") + include("Viz/VizMesh.jl") + # Make the actual plotting routines available + export plotProject!, updatePlot! + # Make plotting constants available for easier use + export MODEL, GRID, MESH, EMPTY, REFINEMENTS, ALL + end +end + +# +# Include interactive mesh functionality for creating, reading, and writing a model for HOHQMesh. +# Note, The visualzation routines are included above in the `__init__` because +# Makie is required. +# + +# Core interactive tool routines for control file readin, curve evaluation, etc. +include("ControlFile/ControlFileOperations.jl") +include("Curves/CurveOperations.jl") +include("Curves/Spline.jl") +include("Misc/DictionaryOperations.jl") +include("Misc/NotificationCenter.jl") +include("Model/Geometry.jl") + +# Project itself and some helper generic and undo functionality +include("Project/Project.jl") +include("Project/Generics.jl") +include("Project/Undo.jl") + +# Front facing API of the interactive tool for the `Project` +include("Project/BackgroundGridAPI.jl") +include("Project/ControlInputAPI.jl") +include("Project/CurvesAPI.jl") +include("Project/ModelAPI.jl") +include("Project/RefinementRegionsAPI.jl") +include("Project/RunParametersAPI.jl") +include("Project/SmootherAPI.jl") + +# Main routine that uses HOHQMesh to generate a mesh from an interactive `Project` +include("Mesh/Meshing.jl") + +# Generic main function to generate a mesh from a control file +# or an interactive mesh `Project` export generate_mesh +# +# Export the front facing interactive mesh functionality, e.g., getter/setter pairs. +# + +# Generic functions for the interactive mesh interface +export new, + add!, + getCurve, + getInnerBoundary, + remove! + +# Functions from `BackgroundGridAPI.jl` +export addBackgroundGrid!, removeBackgroundGrid!, + setBackgroundGridSize!, getBackgroundGridSize, + getBackgroundGridLowerLeft, + getBackgroundGridSteps + +# Functions from `CurvesAPI.jl` +export newParametricEquationCurve, + newEndPointsLineCurve, + newCircularArcCurve, + newSplineCurve, + setCurveName!, getCurveName, + getCurveType, + setXEqn!, getXEqn, + setYEqn!, getYEqn, + setZEqn!, getZEqn, + setStartPoint!, getStartPoint, + setEndPoint!, getEndPoint, + setArcUnits!, getArcUnits, + setArcCenter!, getArcCenter, + setArcStartAngle!, getArcStartAngle, + setArcEndAngle!, getArcEndAngle, + setArcRadius!, getArcRadius, + setSplineNKnots!, getSplineNKnots, + setSplinePoints!, getSplinePoints + +# Functions from `ModelAPI.jl` +export addCurveToOuterBoundary!, + removeOuterBoundaryCurveWithName!, removeOuterBoundaryCurveAtIndex!, + getOuterBoundaryCurveWithName, + insertOuterBoundaryCurveAtIndex!, + addOuterBoundary!, removeOuterBoundary!, + getOuterBoundaryChainList, + addCurveToInnerBoundary!, + removeInnerBoundaryCurve!, removeInnerBoundaryCurveAtIndex!, + insertInnerBoundaryCurveAtIndex!, + removeInnerBoundary!, + addInnerBoundaryWithName!, + getChainIndex, + getInnerBoundaryChainWithName, + getInnerBoundaryCurve + +# Functions from `Project.jl` +export newProject, openProject, saveProject + +# Functions from `RefinementRegionsAPI.jl` +export newRefinementCenter, + newRefinementLine, + addRefinementRegion!, getRefinementRegion, getAllRefinementRegions, + getRefinementRegionCenter, + insertRefinementRegion!, removeRefinementRegion!, + setRefinementType!, getRefinementType, + setRefinementName!, getRefinementName, + setRefinementLocation!, getRefinementLocation, + setRefinementGridSize!, getRefinementGridSize, + setRefinementWidth!, getRefinementWidth, + setRefinementStart!, getRefinementStart, + setRefinementEnd!, getRefinementEnd + +# Functions from `RunParametersAPI.jl` +export addRunParameters!, removeRunParameters!, + setName!, getName, + setPolynomialOrder!, getPolynomialOrder, + setMeshFileFormat!, getMeshFileFormat, + setPlotFileFormat!, getPlotFileFormat, + setFileNames!, getMeshFileName, getPlotFileName, getStatsFileName + +# Functions from `SmootherAPI.jl` +export addSpringSmoother!, removeSpringSmoother!, + setSmoothingStatus!, getSmoothingStatus, + setSmoothingType!, getSmoothingType, + setSmoothingIterations!, getSmoothingIterations + +# Functions from `Undo.jl` +export undo, undoActionName, + redo, redoActionName, + clearUndoRedo + +# Functions from `Meshing.jl`, generate_mesh is already exported +export remove_mesh! + """ generate_mesh(control_file; @@ -115,5 +257,4 @@ Return the path to the directory with some example mesh setups. """ examples_dir() = joinpath(pathof(HOHQMesh) |> dirname |> dirname, "examples") - end # module diff --git a/src/Mesh/Meshing.jl b/src/Mesh/Meshing.jl new file mode 100644 index 00000000..00e1e67d --- /dev/null +++ b/src/Mesh/Meshing.jl @@ -0,0 +1,51 @@ + +""" + generate_mesh(proj::Project) + +Generate a mesh from the information stored in a `Project` created using the +interactive mesh functionality. First a check is made +if a background grid exists and all inner/outer boundary curves are valid. + +This function will then make a HOHQMesh control file from the control dictionary `proj.controlDict` +and use it to call the wrapper function that interfaces with HOHQMesh. The resulting mesh and control files +will be saved to `proj.projectDirectory`. Also, if there is an active plot of the mesh project it +will update to display the generated mesh. + +This function returns the output to `stdout` of the HOHQMesh binary when generating the mesh. +""" +function generate_mesh(proj::Project) +# +# Check to be sure background grid has been created (everything else is defaults) +# + controlDict = getControlDict(proj) + if !haskey(controlDict,"BACKGROUND_GRID") + @warn "A background grid is needed before meshing. Add one and try again." + return nothing + end + + if !modelCurvesAreOK(proj) + @warn "Meshing aborted: Ensure boundary curve segments are in order and boundary curves are closed and try again." + return nothing + end + + saveProject(proj) + fileName = joinpath(proj.projectDirectory,proj.name)*".control" + mesherOutput = generate_mesh(fileName, output_directory = proj.projectDirectory) + println(mesherOutput) + postNotificationWithName(proj,"MESH_WAS_GENERATED_NOTIFICATION",(nothing,)) + return nothing +end + + +""" + remove_mesh!(proj::Project) + +Remove the mesh file from `proj.projectDirectory` and delete the mesh from the plot +""" +function remove_mesh!(proj::Project) + meshFile = getMeshFileName(proj) + rm(meshFile) + proj.xMesh = Float64[] + proj.yMesh = Float64[] + postNotificationWithName(proj,"MESH_WAS_DELETED_NOTIFICATION",(nothing,)) +end diff --git a/src/Misc/DictionaryOperations.jl b/src/Misc/DictionaryOperations.jl new file mode 100644 index 00000000..fca5c34d --- /dev/null +++ b/src/Misc/DictionaryOperations.jl @@ -0,0 +1,70 @@ +#= + Some useful getters for a dictionary +=# + +const arrayRegex = r"(?<=\[).+?(?=\])" + +function realForKeyFromDictionary(key::AbstractString, d::Dict{String,Any}) + v = d[key] + return parse(Float64,v) +end + + +function intForKeyFromDictionary(key::AbstractString, d::Dict{String,Any}) + v = d[key] + return parse(Int64,v) +end + + +function stringForKeyFromDictionary(key::AbstractString, d::Dict{String,Any}) + v = d[key] + return v +end + + +function realArrayForKeyFromDictionary(key::AbstractString, d::Dict{String,Any}) + v = d[key] + values = match(arrayRegex,v) + s = split(values.match,",") + array = [parse(Float64,s[1]),parse(Float64,s[2]),parse(Float64,s[3])] + return array +end + + +function intArrayForKeyFromDictionary(key::AbstractString, d::Dict{String,Any}) + v = d[key] + values = match(arrayRegex,v) + s = split(values.match,",") + array = [parse(Int64,s[1]),parse(Int64,s[2]),parse(Int64,s[3])] + return array +end + + +function keyAndValueFromString(s) + indxOfEqual = findfirst("=",s) + if indxOfEqual === nothing + error("Equal sign = required to distinguish key and value from a string.") + end + key = strip(s[1:indxOfEqual.start-1],[' ','\t']) + value = strip(s[indxOfEqual.stop+1:end],[' ','\t']) + return (key,value) +end + + +function showDescription(d::Dict, pre=1) + todo = Vector{Tuple}() + for (k,v) in d + if typeof(v) <: Dict + push!(todo, (k,v)) + else + println(join(fill(" ", pre)) * "$(repr(k)) => $(repr(v))") + end + end + + for (k,d) in todo + s = "$(repr(k)) => " + println(join(fill(" ", pre)) * s) + showDescription(d, pre+1+length(s)) + end + nothing +end diff --git a/src/Misc/NotificationCenter.jl b/src/Misc/NotificationCenter.jl new file mode 100644 index 00000000..86a5b884 --- /dev/null +++ b/src/Misc/NotificationCenter.jl @@ -0,0 +1,89 @@ + +struct HQMNotification + sender ::Any # Who sent the notification + userInfo ::Tuple # Any necessary data needed +end + +struct HQMNotificationObject + observer::Any + fcn ::Any +end + +HQMNotificationCenter = Dict{String,Vector{HQMNotificationObject}}() +HQMNotificationsON = true + + +""" + addObserver(observer::Any, note::String, fnction::Any) + +fnction is the function to be executed (called) when a +notification of name `note` is given. + +The function called upon notification must have the signature +fnction(observer, sender, args...) +""" +function addObserver(observer::Any, note::String, fnction::Any) + + noteObj = HQMNotificationObject(observer,fnction) + if !haskey(HQMNotificationCenter,note) + HQMNotificationCenter[note] = HQMNotificationObject[] + end + push!(HQMNotificationCenter[note],noteObj) +end + + +""" + unRegisterForNotification(observer::Any, note::String) + +Remove the observer from being notified by the notification `note` +""" +function unRegisterForNotification(observer::Any, note::String) + if haskey(HQMNotificationCenter,note) + global observers = HQMNotificationCenter[note] + + for i = 1:length(observers) + global noteObj = observers[i] + noteObserver = noteObj.observer + if noteObserver === observer + deleteat!(observers,i) + break + end + end + if isempty(observers) + delete!(HQMNotificationCenter,note) + end + end +end + + +""" + postNotificationWithName(sender::Any, name::String, userInfo::Tuple) + +Executes the function associated with the observer for the notification `note` +""" +function postNotificationWithName(sender::Any, note::String, userInfo::Tuple) + if haskey(HQMNotificationCenter,note) && HQMNotificationsON + global observers = HQMNotificationCenter[note] + + for i = 1:length(observers) + global noteObj = observers[i] + f = noteObj.fcn + observer = noteObj.observer + if isnothing(userInfo[1]) + f(observer,sender) + else + f(observer,sender,userInfo...) + end + end + end +end + + +function enableNotifications() + global HQMNotificationsON = true +end + + +function disableNotifications() + global HQMNotificationsON = false +end diff --git a/src/Model/Geometry.jl b/src/Model/Geometry.jl new file mode 100644 index 00000000..bcabca84 --- /dev/null +++ b/src/Model/Geometry.jl @@ -0,0 +1,73 @@ + +const TOP = 1; const LEFT = 2; const BOTTOM = 3; const RIGHT = 4 + +""" +curveBounds(crvPoints::Array{Float64,2}) + +Find the bounds of a single curve, discretized as an array +""" +function curveBounds(crvPoints::Array{Float64,2}) + + s = size(crvPoints) + top = -Inf64 + left = Inf64 + bottom = Inf64 + right = -Inf64 + + for i = 1:s[1] + right = max(right,crvPoints[i,1]) + left = min(left,crvPoints[i,1]) + top = max(top,crvPoints[i,2]) + bottom = min(bottom,crvPoints[i,2]) + end + + bounds = zeros(Float64,4) + bounds[TOP] = top + bounds[LEFT] = left + bounds[BOTTOM] = bottom + bounds[RIGHT] = right + + return bounds +end + + +function chainBounds(chain::Array{Any}) + bounds = emptyBounds() + for crv in chain + crvBounds = curveBounds(crv) + bounds = bboxUnion(bounds,crvBounds) + end + return bounds +end + + +""" + bboxUnion(box1::Array{Float64}, box2::Array{Float64}) + +Returns the union of two bounding boxes +""" +function bboxUnion(box1::Array{Float64}, box2::Array{Float64}) + union = zeros(Float64,4) + union[TOP] = max(box1[TOP] ,box2[TOP]) + union[LEFT] = min(box1[LEFT] ,box2[LEFT]) + union[BOTTOM] = min(box1[BOTTOM],box2[BOTTOM]) + union[RIGHT] = max(box1[RIGHT] ,box2[RIGHT]) + + return union +end + + +""" + emptyBounds() + +Returns an array that will always be ignored when unioned with +another bounding box. +""" +function emptyBounds() + emptee = zeros(Float64,4) + emptee[TOP] = -Inf64 + emptee[LEFT] = Inf64 + emptee[BOTTOM] = Inf64 + emptee[RIGHT] = -Inf64 + return emptee +end diff --git a/src/Project/BackgroundGridAPI.jl b/src/Project/BackgroundGridAPI.jl new file mode 100644 index 00000000..4eae8c73 --- /dev/null +++ b/src/Project/BackgroundGridAPI.jl @@ -0,0 +1,263 @@ + +""" + addBackgroundGrid(proj::Project, bgSize::Array{Float64}) + +Add the background grid block with the grid size to be a 3-vector. Use this when there +is an outer boundary defined in the model. +""" +function addBackgroundGrid!(proj::Project, bgSize::Array{Float64}) + disableUndo() + disableNotifications() + setBackgroundGridSize!(proj, bgSize, "background grid size") + enableUndo() + registerWithUndoManager(proj,removeBackgroundGrid!,(nothing,),"Add Background Grid") + enableNotifications() + postNotificationWithName(proj,"BGRID_DID_CHANGE_NOTIFICATION",(nothing,)) +end + + +""" + addBackgroundGrid!(proj::Project, box::Array{Float64}, N::Array{Int} ) + +Add the background grid block with bounding box = [TOP, LEFT, BOTTOM, RIGHT] +and the number of intervals in each diredction. Use this when there +is _no_ outer boundary defined in the model. +""" +function addBackgroundGrid!(proj::Project, box::Array{Float64}, N::Array{Int}) + disableUndo() + disableNotifications() + proj.bounds = box + proj.userBounds = deepcopy(box) + + dx = zeros(Float64,3) + dx[1] = (box[RIGHT] - box[LEFT])/N[1] + dx[2] = (box[TOP] - box[BOTTOM])/N[2] + + setBackgroundGridSize!(proj, dx, "dx") + setBackgroundGridLowerLeft!(proj,[box[LEFT], box[BOTTOM], 0.0]) + setBackgroundGridSteps!(proj,N) + enableUndo() + registerWithUndoManager(proj,removeBackgroundGrid!,(nothing,),"Add Background Grid") + enableNotifications() + postNotificationWithName(proj,"BGRID_DID_CHANGE_NOTIFICATION",(nothing,)) + return nothing +end + + +""" + addBackgroundGrid!(proj::Project, x0::Array{Float64}, dx::Array{Float64}, N::Array{Int}) + +Add the background grid block using the left corner, x0, the +grid size dx, and the number of intervals in each direction. Use this when there +is _no_ outer boundary defined in the model. This version mimics HOHQMesh's +backgroundGrid block, but the version + + addBackgroundGrid!(proj::Project, box::Array{Float64}, N::Array{Int} ) + +is a lot easier to use. + +TODO: Change HOHQMesh and delete this way to specify the domain and use the bounding box one instead. +""" +function addBackgroundGrid!(proj::Project, x0::Array{Float64}, dx::Array{Float64}, N::Array{Int}) + bgDict = getDictInControlDictNamed(proj,"BACKGROUND_GRID") + disableUndo() + disableNotifications() + setBackgroundGridSize!(proj, dx, "dx") + setBackgroundGridLowerLeft!(proj,x0) + setBackgroundGridSteps!(proj,N) + proj.userBounds[TOP] = x0[2] + N[2]*dx[2] + proj.userBounds[LEFT] = x0[1] + proj.userBounds[BOTTOM] = x0[2] + proj.userBounds[RIGHT] = x0[1] + N[1]*dx[1] + enableUndo() + enableNotifications() + registerWithUndoManager(proj,removeBackgroundGrid!,(nothing,),"Add Background Grid") + postNotificationWithName(proj,"BGRID_DID_CHANGE_NOTIFICATION",(nothing,)) + return nothing +end + + +""" + removeBackgroundGrid!(proj::Project) + +Remove the background grid block from the project. +""" +function removeBackgroundGrid!(proj::Project) + cDict = getControlDict(proj) + registerWithUndoManager(proj,addBackgroundGrid!,(cDict,),"Delete Background Grid") + delete!(cDict,"BACKGROUND_GRID") + postNotificationWithName(proj,"BGRID_DID_CHANGE_NOTIFICATION",(nothing,)) + return nothing +end + + +""" + setBackgroundGridSpacing!(proj::Project, dx::Float64, dy::Float64, dz::Float64 = 0.0) + +User facing function +""" +function setBackgroundGridSize!(proj::Project, dx::Float64, dy::Float64, dz::Float64 = 0.0) + bgDict = getDictInControlDictNamed(proj,"BACKGROUND_GRID") + + newSize = [dx,dy] + if haskey(bgDict,"dx") + oldSpacing = realArrayForKeyFromDictionary("dx", bgDict) + setBackgroundGridSize!(proj, newSize, "dx") + # With the "corner+intervals" setting of the outer boundary deprecated, keep + # the original bounds fixed. + x0 = realArrayForKeyFromDictionary("x0",bgDict) + Nx = round(Int,(proj.userBounds[RIGHT] - proj.userBounds[LEFT]) /dx) + Ny = round(Int,(proj.userBounds[TOP] - proj.userBounds[BOTTOM])/dy) + N = [Nx,Ny,0] + disableNotifications() + setBackgroundGridSteps!(proj,N) + enableNotifications() + else + oldSpacing = realArrayForKeyFromDictionary("background grid size", bgDict) + setBackgroundGridSize!(proj, newSize, "background grid size") + end + postNotificationWithName(proj,"BGRID_DID_CHANGE_NOTIFICATION",(nothing,)) + registerWithUndoManager(proj,setBackgroundGridSize!, + (oldSpacing[1],oldSpacing[2],0.0),"Set Background Grid Spacing") + return nothing +end + + +""" + getBackgroundGridSize(proj::Project) + +Returns the background grid size array. +""" +function getBackgroundGridSize(proj::Project) + bgDict = getDictInControlDictNamed(proj,"BACKGROUND_GRID") + if haskey(bgDict,"dx") + return realArrayForKeyFromDictionary("dx",bgDict) + elseif haskey(bgDict,"background grid size") + return realArrayForKeyFromDictionary("background grid size",bgDict) + else + error("No background grid size is present.") + end +end + + +""" + getBackgroundGridLowerLeft(proj::Project) + +Returns the [x,y] of the lower left point of the background grid. +""" +function getBackgroundGridLowerLeft(proj::Project) + bgDict = getDictInControlDictNamed(proj,"BACKGROUND_GRID") + if haskey(bgDict,"x0") + return realArrayForKeyFromDictionary("x0",bgDict) + else + error("No background grid initial point x0 is present.") + end +end + + +""" + getBackgroundGridSteps(proj::Project) + +Returns the [Nx,Ny,Nz] for the background grid. +""" +function getBackgroundGridSteps(proj::Project) + bgDict = getDictInControlDictNamed(proj,"BACKGROUND_GRID") + if haskey(bgDict,"N") + return intArrayForKeyFromDictionary("N",bgDict) + else + error("No background grid step size is present.") + end +end + + +""" + setBackgroundGridLowerLeft!(proj::Project, x0::Array{Float64}) + +Set the lower left location of the background grid for problems that have no +outer boundary. +""" +function setBackgroundGridLowerLeft!(proj::Project, x0::Array{Float64}) + + bgDict = getDictInControlDictNamed(proj,"BACKGROUND_GRID") + + if haskey(bgDict,"x0") + oldLowerLeft = realArrayForKeyFromDictionary("x0",bgDict) + registerWithUndoManager(proj,setBackgroundGridLowerLeft!, + (oldLowerLeft[1],oldLowerLeft[2],0.0),"Set Background Lower Left") + end + + x0Str = "[$(x0[1]),$(x0[2]),$(x0[3])]" + bgDict["x0"] = x0Str + postNotificationWithName(proj,"BGRID_DID_CHANGE_NOTIFICATION",(nothing,)) + return nothing +end + + +""" + setBackgroundGridSteps!(proj::Project, N::Array{Int}) + +Set how many steps of size setBackgroundGridSpacing in each direction the background grid extends from the +lower left. +""" +function setBackgroundGridSteps!(proj::Project, N::Array{Int}) + bgDict = getDictInControlDictNamed(proj,"BACKGROUND_GRID") + + if haskey(bgDict,"N") + oldN = intArrayForKeyFromDictionary("N",bgDict) + registerWithUndoManager(proj,setBackgroundGridSteps!, + (oldN[1],oldN[2],oldN[3]),"Set Background Steps") + end + + NStr = "[$(N[1]),$(N[2]),$(N[3])]" + bgDict["N"] = NStr + + postNotificationWithName(proj,"BGRID_DID_CHANGE_NOTIFICATION",(nothing,)) + return nothing +end + + +""" + setBackgroundGridSize!(proj::Project, dx::Array{Float64}, key::String) + +Set the grid size dx of an existing background grid withing `proj`. Here, `dx` +is passed an array. +""" +function setBackgroundGridSize!(proj::Project, dx::Array{Float64}, key::String) + setBackgroundGridSize!(proj, dx[1], dx[2], key) + return nothing +end + + +""" + setBackgroundGridSize!(proj::Project, dx::Float64, dy::Float64, key::String) + +Set the grid size dx of an existing background grid within `proj`. Here, the new grid +size in either direction is passed individually with `dx`and `dy`. +""" +function setBackgroundGridSize!(proj::Project, dx::Float64, dy::Float64, key::String) + bgDict = getDictInControlDictNamed(proj,"BACKGROUND_GRID") + + if haskey(bgDict,key) + oldDx = realArrayForKeyFromDictionary(key,bgDict) + registerWithUndoManager(proj,setBackgroundGridSize!, + (oldDx[1],oldDx[2],oldDx[3]),"Set Background Size") + end + + dxStr = "[$(dx),$(dy),$(0.0)]" + bgDict[key] = dxStr + postNotificationWithName(proj,"BGRID_DID_CHANGE_NOTIFICATION",(nothing,)) + return nothing +end + + +""" + addBackgroundGrid!(proj::Project, dict::Dict{String,Any}) + +Used only for undo/redo. +""" +function addBackgroundGrid!(proj::Project, dict::Dict{String,Any}) + controlDict = getControlDict(proj) + controlDict["BACKGROUND_GRID"] = dict + return nothing +end + diff --git a/src/Project/ControlInputAPI.jl b/src/Project/ControlInputAPI.jl new file mode 100644 index 00000000..6f27e44c --- /dev/null +++ b/src/Project/ControlInputAPI.jl @@ -0,0 +1,37 @@ + +function getControlDict(proj::Project) + if haskey(proj.projectDictionary,"CONTROL_INPUT") + return proj.projectDictionary["CONTROL_INPUT"] + else + controlDict = Dict{String,Any}() + proj.projectDictionary["CONTROL_INPUT"] = controlDict + controlDict["TYPE"] = "CONTROL_INPUT" + return controlDict + end +end + + +function getDictInControlDictNamed(proj::Project,name::String) + controlDict = getControlDict(proj) + + if haskey(controlDict,name) + return controlDict[name] + else + d = Dict{String,Any}() + controlDict[name] = d + d["TYPE"] = name + return d + end +end + + +function getListInControlDictNamed(proj::Project,name::String) + dict = getDictInControlDictNamed(proj::Project,name::String) + if haskey(dict,"LIST") + return dict["LIST"] + else + lst = [] + dict["LIST"] = lst + return lst + end +end diff --git a/src/Project/CurvesAPI.jl b/src/Project/CurvesAPI.jl new file mode 100644 index 00000000..a6d37aa5 --- /dev/null +++ b/src/Project/CurvesAPI.jl @@ -0,0 +1,484 @@ + +""" + newParametricEquationCurve(name::String, + xEqn::String, + yEqn::String, + zEqn::String = "z(t) = 0.0" ) + +Creates and returns a new parametricEquationCurve in the form of a Dictionary +""" +function newParametricEquationCurve(name::String, + xEqn::String, + yEqn::String, + zEqn::String = "z(t) = 0.0" ) + + crv = Dict{String,Any}() + crv["TYPE"] = "PARAMETRIC_EQUATION_CURVE" + disableNotifications() + disableUndo() + setCurveName!(crv,name) + setXEqn!(crv,xEqn) + setYEqn!(crv,yEqn) + setZEqn!(crv,zEqn) + enableUndo() + enableNotifications() + return crv +end + + +""" + newEndPointsLineCurve(name::String, xStart::Array{Float64},xEnd::Array[Float64]) + +Creates and returns a new curve defined by its end points in the form of a Dictionary +""" +function newEndPointsLineCurve(name::String, + xStart::Array{Float64}, + xEnd::Array{Float64}) + crv = Dict{String,Any}() + crv["TYPE"] = "END_POINTS_LINE" + disableNotifications() + disableUndo() + setCurveName!(crv,name) + setStartPoint!(crv,xStart) + setEndPoint!(crv,xEnd) + enableNotifications() + enableUndo() + return crv +end + + +""" + newCircularArcCurve(name::String, center::Array{Float64}, + startAngle::Float64, endAngle::Float64, + units::String) + +Creates and returns a new circular arc curve in the form of a Dictionary +""" +function newCircularArcCurve(name::String, + center::Array{Float64}, + radius::Float64, + startAngle::Float64, + endAngle::Float64, + units::String = "degrees") + + arc = Dict{String,Any}() + arc["TYPE"] = "CIRCULAR_ARC" + disableNotifications() + disableUndo() + setCurveName!(arc,name) + setArcUnits!(arc,units) + setArcCenter!(arc,center) + setArcStartAngle!(arc,startAngle) + setArcEndAngle!(arc,endAngle) + setArcRadius!(arc,radius) + enableNotifications() + enableUndo() + return arc +end + + +""" + newSplineCurve(name::String, nKnots::Int, data::Array{Float64,4}) + +Returns a spline curve given the number of knots and the array of knots. +""" +function newSplineCurve(name::String, nKnots::Int, data::Matrix{Float64}) + spline = Dict{String,Any}() + spline["TYPE"] = "SPLINE_CURVE" + disableNotifications() + disableUndo() + setCurveName!(spline,name) + setSplineNKnots!(spline,nKnots) + setSplinePoints!(spline,data) + enableNotifications() + enableUndo() + return spline +end + + +""" + newSplineCurve(name::String, dataFile::String) + +Returns a spline curve given a data file that contains the number of knots +on the first line, and the spline data following that. +""" +function newSplineCurve(name::String, dataFile::String) + + spline = Dict{String,Any}() + open(dataFile,"r") do f + nKnots = parse(Int,readline(f)) + splineDataArray = zeros(Float64,nKnots,4) + for i = 1:nKnots + currentLine = split(readline(f)) + for j = 1:4 + splineDataArray[i,j] = parse(Float64,currentLine[j]) + end + end + spline = newSplineCurve(name, nKnots, splineDataArray) + end + return spline +end + + +#""" +# duplicateCurve(crv::Dict{String,Any}, newName::String) +# +#Duplicate the given curve giving it the new name. +#""" +# function duplicateCurve(crv::Dict{String,Any}, newName::String) +# disableNotifications() +# disableUndo() + +# duplicate = deepcopy(crv) +# setCurveName!(duplicate,newName) + +# enableNotifications() +# enableUndo() +# return duplicate +# end + + +""" + setCurveName!(curveDict, name) + +Set the name of the curve represented by curveDict. +""" +function setCurveName!(crv::Dict{String,Any}, name::String) + if haskey(crv,"name") + oldName = crv["name"] + registerWithUndoManager(crv,setCurveName!, (oldName,), "Set Curve Name") + postNotificationWithName(crv,"CURVE_DID_CHANGE_NAME_NOTIFICATION",(oldName,)) + end + crv["name"] = name +end + + +""" + getCurveName(crv::Dict{String,Any}) +""" +function getCurveName(crv::Dict{String,Any}) + return crv["name"] +end + + +""" + getCurveType(crv::Dic{String,Any}) + + Get the type of the curve, `END_POINTSLINE_CURVE`, `PARAMETRIC_EQUATION_CURVE`, + `SPLINE_CURVE`, or `CIRCULAR_ARC` as a string. +""" +function getCurveType(crv::Dict{String,Any}) + return crv["TYPE"] +end + + +""" + setXEqn!(parametricEquationCurve, eqn) + +For a parametric equation, set the x-equation. +""" +function setXEqn!(crv::Dict{String,Any}, eqn::String) + if haskey(crv,"xEqn") + oldEqn = crv["xEqn"] + registerWithUndoManager(crv,setXEqn!, (eqn,), "Set X Equation") + end + crv["xEqn"] = eqn + postNotificationWithName(crv,"CURVE_DID_CHANGE_NOTIFICATION",(nothing,)) +end + + +""" + getXEqn(crv::Dict{String,Any}) +""" +function getXEqn(crv::Dict{String,Any}) + if(haskey(crv,"xEqn")) + return crv["xEqn"] + end + return nothing +end + + +""" + setYEqn!(parametricEquationCurve, eqn) + +For a parametric equation, set the y-equation. +""" +function setYEqn!(crv::Dict{String,Any}, eqn::String) + if haskey(crv,"yEqn") + oldEqn = crv["yEqn"] + registerWithUndoManager(crv,setYEqn!, (eqn,), "Set Y Equation") + end + crv["yEqn"] = eqn + postNotificationWithName(crv,"CURVE_DID_CHANGE_NOTIFICATION",(nothing,)) +end + + +""" + getYEqn(crv::Dict{String,Any}) +""" +function getYEqn(crv::Dict{String,Any}) + if(haskey(crv,"yEqn")) + return crv["yEqn"] + end + return nothing +end + + +""" + setZEqn!(parametricEquationCurve, eqn) + +For a parametric equation, set the zEqn-equation. +""" +function setZEqn!(crv::Dict{String,Any}, eqn::String) + if haskey(crv,"zEqn") + oldEqn = crv["zEqn"] + registerWithUndoManager(crv,setZEqn!, (eqn,), "Set Z Equation") + end + crv["zEqn"] = eqn + postNotificationWithName(crv,"CURVE_DID_CHANGE_NOTIFICATION",(nothing,)) +end + + +""" + getZEqn(crv::Dict{String,Any}) +""" +function getZEqn(crv::Dict{String,Any}) + if(haskey(crv,"zEqn")) + return crv["zEqn"] + end + return nothing +end + + +""" + setStartPoint!(crv::Dict{String,Any}, point::Array{Float64}) + +Set the start point for a line curve. +""" +function setStartPoint!(crv::Dict{String,Any}, point::Array{Float64}) + pStr = "[$(point[1]),$(point[2]),$(point[3])]" + setStartPoint!(crv,pStr) +end + + +function setStartPoint!(crv::Dict{String,Any}, pointAsString::String) + key = "xStart" + if haskey(crv,key) + oldPt = crv[key] + registerWithUndoManager(crv,setStartPoint!, (oldPt,), "Set Start Point") + end + crv[key] = pointAsString + postNotificationWithName(crv,"CURVE_DID_CHANGE_NOTIFICATION",(nothing,)) +end + + +""" + getStartPoint(crv::Dict{String,Any}, point::Array{Float64}) + +Get the start point for a line curve as an array +""" +function getStartPoint(crv::Dict{String,Any}) + return realArrayForKeyFromDictionary("xStart",crv) +end + + +""" + setEndPoint!(crv::Dict{String,Any}, point::Array{Float64}) + +Set the end point for a line curve. +""" +function setEndPoint!(crv::Dict{String,Any}, point::Array{Float64}) + pStr = "[$(point[1]),$(point[2]),$(point[3])]" + setEndPoint!(crv,pStr) +end + + +function setEndPoint!(crv::Dict{String,Any}, pointAsString::String) + key = "xEnd" + if haskey(crv,key) + oldPt = crv[key] + registerWithUndoManager(crv,setEndPoint!, (oldPt,), "Set End Point") + end + crv[key] = pointAsString + postNotificationWithName(crv,"CURVE_DID_CHANGE_NOTIFICATION",(nothing,)) +end + + +""" + getEndPoint(crv::Dict{String,Any}, point::Array{Float64}) + +Get the end point for a line curve as an array. +""" +function getEndPoint(crv::Dict{String,Any}) + return realArrayForKeyFromDictionary("xEnd",crv) +end + + +""" + setArcUnits(crv::Dict{String,Any}, units::String) + +Set the units for the start and end angles of a circular arc curve. +""" +function setArcUnits!(arc::Dict{String,Any}, units::String) + if units == "degrees" || units == "radians" + key = "units" + if haskey(arc,key) + oldUnits = arc[key] + registerWithUndoManager(arc,setArcUnits!, (oldUnits,), "Set Arc Units") + end + arc[key] = units + postNotificationWithName(arc,"CURVE_DID_CHANGE_NOTIFICATION",(nothing,)) + else + @warn "Units must either be `degrees` or `radians`. Try setting `units` again." + end +end + + +""" + getArcUnits(crv::Dict{String,Any}, units::String) + +Get the units for the start and end angles of a circular arc curve. +""" +function getArcUnits(arc::Dict{String,Any}) + return arc["units"] +end + + +""" + setArcCenter!(crv::Dict{String,Any}, point::Array{Float64}) + +Set the center of a circular arc. +""" +function setArcCenter!(arc::Dict{String,Any}, point::Array{Float64}) + pStr = "[$(point[1]),$(point[2]),$(point[3])]" + setArcCenter!(arc,pStr) +end +function setArcCenter!(arc::Dict{String,Any}, pointAsString::String) + key = "center" + if haskey(arc,key) + oldVal = arc[key] + registerWithUndoManager(arc,setArcCenter!, (oldVal,), "Set Arc Center") + end + + arc[key] = pointAsString + postNotificationWithName(arc,"CURVE_DID_CHANGE_NOTIFICATION",(nothing,)) +end + + +""" + getArcCenter(crv::Dict{String,Any}, point::Array{Float64}) + +Get the center of a circular arc as an array +""" +function getArcCenter(arc::Dict{String,Any}) + return realArrayForKeyFromDictionary("center",arc) +end + + +""" + setArcStartAngle!(arc::Dict{String,Any}, angle::Float64) +""" +function setArcStartAngle!(arc::Dict{String,Any}, angle::Float64) + key = "start angle" + if haskey(arc,key) + oldVal = parse(Float64,arc[key]) + registerWithUndoManager(arc,setArcStartAngle!, (oldVal,), "Set Arc Start Angle") + end + arc[key] = string(angle) + postNotificationWithName(arc,"CURVE_DID_CHANGE_NOTIFICATION",(nothing,)) +end + + +""" + getArcStartAngle(arc::Dict{String,Any}, angle::Float64) +""" +function getArcStartAngle(arc::Dict{String,Any}) + return parse(Float64,arc["start angle"]) +end + + +""" + setArcEndAngle!(arc::Dict{String,Any}, angle::Float64) +""" +function setArcEndAngle!(arc::Dict{String,Any}, angle::Float64) + key = "end angle" + if haskey(arc,key) + oldVal = parse(Float64,arc[key]) + registerWithUndoManager(arc,setArcEndAngle!, (oldVal,), "Set Arc Start Angle") + end + arc[key] = string(angle) + postNotificationWithName(arc,"CURVE_DID_CHANGE_NOTIFICATION",(nothing,)) +end + + +""" + getArcEndAngle(arc::Dict{String,Any}, angle::Float64) +""" +function getArcEndAngle(arc::Dict{String,Any}) + return parse(Float64,arc["end angle"]) +end + + +""" + setArcRadius!(arc::Dict{String,Any}, radius::Float64) +""" +function setArcRadius!(arc::Dict{String,Any}, radius::Float64) + key = "radius" + if haskey(arc,key) + oldVal = parse(Float64,arc[key]) + registerWithUndoManager(arc,setArcRadius!, (oldVal,), "Set Arc Radius") + end + arc[key] = string(radius) + postNotificationWithName(arc,"CURVE_DID_CHANGE_NOTIFICATION",(nothing,)) +end + + +""" + getArcRadius(arc::Dict{String,Any}, radius::Float64) +""" +function getArcRadius(arc::Dict{String,Any}) + return parse(Float64,arc["radius"]) +end + + +""" + setSplineNKnots!(spline::Dict{String,Any}, nKnots::Int) +""" +function setSplineNKnots!(spline::Dict{String,Any}, nKnots::Int) + key = "nKnots" + if haskey(spline,key) + oldVal = parse(Int,spline[key]) + registerWithUndoManager(spline,setSplineNKnots!, (oldVal,), "Set Spline Knots") + end + spline["nKnots"] = string(nKnots) + postNotificationWithName(spline,"CURVE_DID_CHANGE_NOTIFICATION",(nothing,)) +end + + +""" + getSplineNKnots(spline::Dict{String,Any}) +""" +function getSplineNKnots(spline::Dict{String,Any}) + return parse(Int,spline["nKnots"]) +end + + +""" + setSplinePoints!(spline::Dict{String,Any},points::Array{Float64,4}) +""" +function setSplinePoints!(spline::Dict{String,Any},points::Matrix{Float64}) + key = "SPLINE_DATA" + if haskey(spline,key) + registerWithUndoManager(spline,setSplinePoints!, (spline["SPLINE_DATA"],), "Set Spline Points") + end + spline["SPLINE_DATA"] = points + postNotificationWithName(spline,"CURVE_DID_CHANGE_NOTIFICATION",(nothing,)) +end + + +""" + getSplinePoints(spline::Dict{String,Any}) +""" +function getSplinePoints(spline::Dict{String,Any}) + return spline["SPLINE_DATA"] +end diff --git a/src/Project/Generics.jl b/src/Project/Generics.jl new file mode 100644 index 00000000..92d15429 --- /dev/null +++ b/src/Project/Generics.jl @@ -0,0 +1,150 @@ +# +# Creating curves +# +""" + new(name::String, + xEqn::String, + yEqn::String, + zEqn::String = "z(t) = 0.0" ) + +Create a new parametric equation curve. +""" +function new(name::String, + xEqn::String, + yEqn::String, + zEqn::String = "z(t) = 0.0" ) + return newParametricEquationCurve(name, xEqn, yEqn, zEqn) +end + + +""" + new(name::String, + xStart::Array{Float64}, + xEnd::Array{Float64}) + +Create a new line defined by its end points. +""" +function new(name::String, + xStart::Array{Float64}, + xEnd::Array{Float64}) + return newEndPointsLineCurve(name, xStart, xEnd) +end + + +""" + new(name::String, + center::Array{Float64}, + radius::Float64, + startAngle::Float64, + endAngle::Float64, + units::String) + +Create a new circular arc. +""" +function new(name::String, + center::Array{Float64}, + radius::Float64, + startAngle::Float64, + endAngle::Float64, + units::String = "degrees") + return newCircularArcCurve(name,center,radius,startAngle,endAngle,units) +end + + +""" + new(name::String, dataFile::String) + +Create a spline curve from the contents of a data file. +""" +function new(name::String, dataFile::String) + return newSplineCurve(name, dataFile) +end + + +""" + new(name::String, nKnots::Int, data::Matrix{Float64}) + +Create a spline curve from an array of knots +""" +function new(name::String, nKnots::Int, data::Matrix{Float64}) + return newSplineCurve(name, nKnots, data) +end + + +# +# Adding curves to a model +# +""" + add!(proj::Project, obj::Dict{String,Any}) + +Add a curve to the outer boundary or a refinement reion to +the project +""" +function add!(proj::Project, obj::Dict{String,Any}) + if obj["TYPE"] == "REFINEMENT_CENTER" || obj["TYPE"] == "REFINEMENT_LINE" + addRefinementRegion!(proj, obj) + else + addCurveToOuterBoundary!(proj, obj) + end +end + + +""" + add!(proj::Project, crv::Dict{String,Any}, boundaryName::String) + +Add a curve to the inner boundary named `boundaryName`. +""" +function add!(proj::Project, crv::Dict{String,Any}, boundaryName::String) + addCurveToInnerBoundary!(proj, crv, boundaryName) +end + + +""" +getCurve(proj::Project, curveName::String) + +Get the curve with name `curveName` from the outer boundary. +""" +function getCurve(proj::Project, curveName::String) + return getOuterBoundaryCurveWithName(proj, curveName) +end + + +""" +getCurve(proj::Project, curveName::String, boundaryName::String) + +Get the curve named `curveName` from the inner boundary named `boundaryName` +""" +function getCurve(proj::Project, curveName::String, boundaryName::String) + return getInnerBoundaryCurve(proj, curveName, boundaryName) +end + + +""" + getInnerBoundary(proj::Project, name::String) + +Get the chain of curves from the inner boundary with name `name`. +""" +function getInnerBoundary(proj::Project, name::String) + return getInnerBoundaryChainWithName(proj, name) +end + + +""" + remove!(proj::Project, curveName::String) + +Delete the curve named curveName from the outer boundary +""" +function remove!(proj::Project, curveName::String) + removeOuterBoundaryCurveWithName!(proj, curveName) +end + + +""" + remove!(proj::Project, curveName::String, innerBoundaryName::String) + +Delete the curve named curveName from the inner boundary named innerBoundaryName +""" +function remove!(proj::Project, curveName::String, innerBoundaryName::String) + removeInnerBoundaryCurve!(proj, curveName, innerBoundaryName) +end + diff --git a/src/Project/ModelAPI.jl b/src/Project/ModelAPI.jl new file mode 100644 index 00000000..bfa5ebc6 --- /dev/null +++ b/src/Project/ModelAPI.jl @@ -0,0 +1,487 @@ +# +# -------------------------------------------------------------------------------------- +# OUTER BOUNDARY FUNCTIONS +# -------------------------------------------------------------------------------------- +# +""" + addCurveToOuterBoundary!(proj::Project, crv::Dict{String,Any}) + +Add a curve to the outer boundary. The curves must be added in order counter-clockwise +""" +function addCurveToOuterBoundary!(proj::Project, crv::Dict{String,Any}) + chain = getOuterBoundaryChainList(proj) + i = chainInsertionIndex(crv,chain) + + enableNotifications() + insertOuterBoundaryCurveAtIndex!(proj,crv,i) + + enableUndo() + registerWithUndoManager(proj,removeOuterBoundaryCurveWithName!,(crv["name"],),"Add Outer Boundary Curve") + println("Added curve ",getCurveName(crv)," to the outer boundary chain.") +end + + +""" + removeOuterBoundaryCurveWithName!(proj::Project, name::String) + +Remove the named curve in the outer boundary. +""" +function removeOuterBoundaryCurveWithName!(proj::Project, name::String) + lst = getOuterBoundaryChainList(proj) + indx = getChainIndex(lst,name) + if indx > 0 + proj.backgroundGridShouldUpdate = true + removeOuterBoundaryCurveAtIndex!(proj,indx) # posts undo/notification + else + # `name` to be deleted does not lie in outer boundary chain. Throw an error. + error("No curve ", name, " in boundary Outer. Try again.") + end +end + + +""" + getOuterBoundaryCurveWithName(proj::Project, name::String) +""" +function getOuterBoundaryCurveWithName(proj::Project, name::String) + lst = getOuterBoundaryChainList(proj) + for crv in lst + if crv["name"] == name + return crv + end + end +end + + +""" + insertOuterBoundaryCurveAtIndex!(proj::Project, crv::Dict{String,Any}, indx::Int) + +Insert a curve into the outer boundary chain at the specified index. +""" +function insertOuterBoundaryCurveAtIndex!(proj::Project, crv::Dict{String,Any}, indx::Int) + lst = getOuterBoundaryChainList(proj) + insert!(lst,indx,crv) + insert!(proj.outerBndryPoints,indx,curvePoints(crv,defaultPlotPts)) + insert!(proj.outerBndryNames,indx,crv["name"]) + proj.backgroundGridShouldUpdate = true + registerWithUndoManager(proj,removeOuterBoundaryCurveAtIndex!,(indx,),"Add Outer Boundary Curve") + postNotificationWithName(proj,"MODEL_DID_CHANGE_NOTIFICATION",(nothing,)) +end + + +""" + removeOuterBoundaryCurveAtIndex!(proj::Project, indx::Int) + +Remove a curve from the outer boundary chain at the specified index. +""" +function removeOuterBoundaryCurveAtIndex!(proj::Project, indx::Int) + lst = getOuterBoundaryChainList(proj) + crv = lst[indx] + deleteat!(lst,indx) + deleteat!(proj.outerBndryNames,indx) + deleteat!(proj.outerBndryPoints,indx) + proj.backgroundGridShouldUpdate = true + registerWithUndoManager(proj,insertOuterBoundaryCurveAtIndex!,(crv,indx),"Remove Outer Boundary Curve") + postNotificationWithName(proj,"MODEL_DID_CHANGE_NOTIFICATION",(nothing,)) +end + + +""" + addOuterBoundary!(proj::Project, outerBoundary::Dict{String,Any}) + +Add an empty outer boundary to the project. There can be only one. +This function is only used as part of an undo operation removing the outer boundary. +""" +function addOuterBoundary!(proj::Project, outerBoundary::Dict{String,Any}) + model = getModelDict(proj) + # Recover the complete outer boundary dictionary + model["OUTER_BOUNDARY"] = outerBoundary + # Recover the outer boundary points and names for each member of the chain (necessary for plotting) + chain = getOuterBoundaryChainList(proj) + for (i, crv) in enumerate(chain) + crvPoints = curvePoints(crv, defaultPlotPts) + push!(proj.outerBndryPoints, crvPoints) + push!(proj.outerBndryNames , crv["name"]) + end + proj.backgroundGridShouldUpdate = true + registerWithUndoManager(proj,removeOuterBoundary!, (nothing,), "Add Outer Boundary") + postNotificationWithName(proj,"MODEL_DID_CHANGE_NOTIFICATION",(nothing,)) +end + + +""" + removeOuterBoundary!(proj::Project) + +Remove the outer boundary curve if it exists. +""" +function removeOuterBoundary!(proj::Project) + modelDict = getModelDict(proj) + if haskey(modelDict,"OUTER_BOUNDARY") + ob = modelDict["OUTER_BOUNDARY"] + registerWithUndoManager(proj,addOuterBoundary!, (ob,), "Remove Outer Boundary") + delete!(modelDict,"OUTER_BOUNDARY") + proj.outerBndryPoints = Any[] + proj.outerBndryNames = String[] + proj.backgroundGridShouldUpdate = true + postNotificationWithName(proj,"MODEL_DID_CHANGE_NOTIFICATION",(nothing,)) + end +end + + +# function addOuterBoundary(proj::Project,obDict::Dict{String,Any}) +# modelDict = getModelDict(proj) +# modelDict["OUTER_BOUNDARY"] = obDict +# end + + +""" + getOuterBoundaryChainList(proj::Project) + +Get the array of outer boundary curves. +""" +function getOuterBoundaryChainList(proj::Project) + outerBndryDict = getDictInModelDictNamed(proj,"OUTER_BOUNDARY") + if haskey(outerBndryDict,"LIST") + lst = outerBndryDict["LIST"] + return lst + else + lst = Dict{String,Any}[] + outerBndryDict["LIST"] = lst + return lst + end +end +# +# -------------------------------------------------------------------------------------- +# INNER BOUNDARY FUNCTIONS +# -------------------------------------------------------------------------------------- +# +""" + addCurveToInnerBoundary!(proj::Project, crv::Dict{String,Any}, boundaryName::String) + +Add a curve to the inner boundary with name `boundaryName`. If an inner boundary of that name +does not exist, one is created. +""" +function addCurveToInnerBoundary!(proj::Project, crv::Dict{String,Any}, boundaryName::String) + + i, chain = getInnerBoundaryChainWithName(proj,boundaryName) + curveList = chain["LIST"] + j = chainInsertionIndex(crv,curveList) + + enableNotifications() + insertInnerBoundaryCurveAtIndex!(proj,crv,j,boundaryName) + enableUndo() + registerWithUndoManager(proj,removeInnerBoundaryCurve!, + (crv["name"],boundaryName), + "Add Inner Boundary Curve") + println("Added curve ",getCurveName(crv)," to the ",boundaryName," chain.") +end + +""" + removeInnerBoundaryCurve!(proj::Project, name::String, chainName::String) + +Remove the curve with `name` from an inner boundary chain with `chainName`. +""" +function removeInnerBoundaryCurve!(proj::Project, name::String, chainName::String) + i, chain = getInnerBoundaryChainWithName(proj,chainName) + lst = chain["LIST"] + + # Go through `chainName` and check if the passed `name` is present in said chain + name_check = 0 + for (i,dict) in enumerate(lst) + if dict["name"] == name + name_check += 1 + end + end + + if isempty(lst) + # When the chain is empty, `chainName` was not present before the call. + # Throw an error and remove the empty chain otherwise plotting routine breaks. + ibChains = getAllInnerBoundaries(proj) + deleteat!(ibChains,i) + deleteat!(proj.innerBoundaryChainNames,i) + deleteat!(proj.innerBoundaryNames,i) + error("No curve ", name, " in boundary ", chainName, ". Try again.") + elseif name_check == 0 + # Situation where `chainName` already exists but the `name` to be deleted that + # was passed does not lie in that `chainName`. Throw an error. + error("No curve ", name, " in boundary ", chainName, ". Try again.") + end + indx = getChainIndex(lst,name) + removeInnerBoundaryCurveAtIndex!(proj,indx,chainName) +end + + +""" + insertInnerBoundaryCurveAtIndex!(proj::Project, crv::Dict{String,Any}, + indx::Int, boundaryName::String) + +Insert a curve `crv` into an inner boundary chain `boundaryName` +at the specified index `indx`. +""" +function insertInnerBoundaryCurveAtIndex!(proj::Project, crv::Dict{String,Any}, + indx::Int, boundaryName::String) + i, chain = getInnerBoundaryChainWithName(proj,boundaryName) + lst = chain["LIST"] + insert!(lst,indx,crv) + + if i > length(proj.innerBoundaryPoints) # New inner boundary chain + a = [] + push!(a,curvePoints(crv,defaultPlotPts)) + push!(proj.innerBoundaryPoints,a) + else + innerBoundaryPoints = proj.innerBoundaryPoints[i] + insert!(innerBoundaryPoints,indx,curvePoints(crv,defaultPlotPts)) + end + insert!(proj.innerBoundaryNames[i],indx,crv["name"]) + + proj.backgroundGridShouldUpdate = true + postNotificationWithName(proj,"MODEL_DID_CHANGE_NOTIFICATION",(nothing,)) +end + + +""" + removeInnerBoundaryCurveAtIndex!(proj::Project, indx::Int, chainName::String) + +Remove the curve at index `indx` from an inner boundary chain with `chainName`. +""" +function removeInnerBoundaryCurveAtIndex!(proj::Project, indx::Int, chainName::String) + i, chain = getInnerBoundaryChainWithName(proj,chainName) + lst = chain["LIST"] + if indx > 0 + crv = lst[indx] + deleteat!(lst, indx) + if isempty(lst) # Boundary chain contained a single curve + # Complete removal. Requires a different function to be posted + # in the Undo Manager + removeInnerBoundary!(proj::Project, chainName::String) + else # Boundary chain contained more than one curve + deleteat!(proj.innerBoundaryNames[i],indx) + deleteat!(proj.innerBoundaryPoints[i],indx) + registerWithUndoManager(proj,insertInnerBoundaryCurveAtIndex!, + (crv,indx,chainName), + "Remove Inner Boundary Curve") + end + postNotificationWithName(proj,"MODEL_DID_CHANGE_NOTIFICATION",(nothing,)) + end +end + + +""" + removeInnerBoundary!(proj::Project, chainName::String) + +Remove an entire inner boundary. +""" +function removeInnerBoundary!(proj::Project, chainName::String) + i, crv = getInnerBoundaryChainWithName(proj, chainName) + registerWithUndoManager(proj,insertInnerBoundaryAtIndex!, + (chainName,i,crv,proj.innerBoundaryPoints[i],proj.innerBoundaryNames[i]), + "Remove Inner Boundary") + + deleteat!(proj.innerBoundaryChainNames, i) + deleteat!(proj.innerBoundaryPoints, i) + deleteat!(proj.innerBoundaryNames, i) + ibChains = getAllInnerBoundaries(proj) + deleteat!(ibChains,i) + postNotificationWithName(proj,"MODEL_DID_CHANGE_NOTIFICATION",(nothing,)) +end + + +""" + insertInnerBoundaryAtIndex!(proj::Project, chainName::String, indx::Int, chain::??) + +Insert an entire inner boundary. Primarily meant for undo operation. +""" +function insertInnerBoundaryAtIndex!(proj::Project, chainName::String, i::Int, chain::Dict{String, Any}, + bPoints::Vector{Any}, bNames::Vector{String}) + + lst = getAllInnerBoundaries(proj::Project) + insert!(lst,i,chain) + insert!(proj.innerBoundaryChainNames,i,chainName) + insert!(proj.innerBoundaryPoints,i,bPoints) + insert!(proj.innerBoundaryNames,i,bNames) + registerWithUndoManager(proj,removeInnerBoundary!, + (chainName,), + "Remove Inner Boundary") + postNotificationWithName(proj,"MODEL_DID_CHANGE_NOTIFICATION",(nothing,)) +end + + +""" + addInnerBoundaryWithName!(proj::Project,name::String) + +Create a new empty inner boundary with the given name. +""" +function addInnerBoundaryWithName!(proj::Project,name::String) +# +# Create a new chain +# + bndryChain = Dict{String,Any}() + bndryChain["name"] = name + bndryChain["TYPE"] = "CHAIN" + bndryCurves = Dict{String,Any}[] + bndryChain["LIST"] = bndryCurves + + innerBoundariesList = getAllInnerBoundaries(proj) + push!(innerBoundariesList,bndryChain) +# +# Prepare for plotting +# + push!(proj.innerBoundaryChainNames,name) + componentNames = String[] + push!(proj.innerBoundaryNames,componentNames) + + return bndryChain +end + + +function getChainIndex(chain::Vector{Dict{String, Any}},name) + for (i,dict) in enumerate(chain) + if dict["name"] == name + return i + end + end + return 0 +end +""" + getAllInnerBoundaries(proj::Project) + +Returns an array of the inner boundaries +""" + + +function getAllInnerBoundaries(proj::Project) + innerBndryDict = getDictInModelDictNamed(proj,"INNER_BOUNDARIES") + if haskey(innerBndryDict,"LIST") + lst = innerBndryDict["LIST"] + return lst + else + lst = [] + innerBndryDict["LIST"] = lst + return lst + end + return nothing +end + + +""" + getInnerBoundaryWithName(proj::Project, name::String) + +Get the inner boundary CHAIN with the given name. If one does not exist, it +is created. +""" +function getInnerBoundaryChainWithName(proj::Project, name::String) + lst = getAllInnerBoundaries(proj::Project) + # + # See if there is an inner boundary with that name + # + l = length(lst) + i = 0 + if l > 0 + for chain in lst + bCurveName = chain["name"] + i = i + 1 + if bCurveName == name + return i, chain + end + end + end + # + # If not, create one + # + chain = addInnerBoundaryWithName!(proj,name) + return l+1, chain +end +""" + +""" +function getInnerBoundaryCurve(proj::Project, curveName::String, boundaryName::String) + i, chain = getInnerBoundaryChainWithName(proj, boundaryName) + lst = chain["LIST"] + for crv in lst + if crv["name"] == curveName + return crv + end + end + @warn "No curve "*curveName*" in boundary "*boundaryName*". Try again." + return nothing +end + + +""" + innerBoundaryIndices(proj::Project, curveName::String) + +Returns (curveIndex,chainIndex) for the location of the curve named `curveName` +in it's inner boundary chain. +""" +function innerBoundaryIndices(proj::Project, curveName::String) +# +# For each inner boundary curve chain +# + chains = getAllInnerBoundaries(proj) + for (j,chain) in enumerate(chains) + crvList = chain["LIST"] + for (i,crv) in enumerate(crvList) + if crv["name"] == curveName + return i,j + end + end + end + return (0,0) +end + +#= + CHAIN OPERATIONS +=# +function chainInsertionIndex(crv::Dict{String,Any}, chainList::Vector{Dict{String, Any}}) +# +# See if the endpoints of crv match up to any of the curves in the chainList. If so, +# return the index where crv should be inserted into the list. +# + if isempty(chainList) + return 1 # Make crv the start of the chain. + end +# + nCurves = length(chainList) + if curvesMeet(chainList[nCurves],crv) + return nCurves+1 # Check first in likely case that user inputs in order + end +# +# Search though list of curves to see if the start of crv matches +# the end of one of the curves already in the chain. Linear search because +# it's easy and likely the list will not be that large. +# + for i in 1:nCurves + if curvesMeet(chainList[i],crv) + return i+1 # Add after the curve that matches. + end + end + + return nCurves+1 # No match, so just append to the list +end + +#= + OTHER +=# +function getModelDict(proj::Project) + if haskey(proj.projectDictionary,"MODEL") + return proj.projectDictionary["MODEL"] + else + modelDict = Dict{String,Any}() + proj.projectDictionary["MODEL"] = modelDict + modelDict["TYPE"] = "MODEL" + return modelDict + end +end + + +function getDictInModelDictNamed(proj::Project,name::String) + modelDict = getModelDict(proj) + + if haskey(modelDict,name) + return modelDict[name] + else + d = Dict{String,Any}() + modelDict[name] = d + d["TYPE"] = name + return d + end +end diff --git a/src/Project/Project.jl b/src/Project/Project.jl new file mode 100644 index 00000000..dc42e445 --- /dev/null +++ b/src/Project/Project.jl @@ -0,0 +1,472 @@ +#= + The Project is the controller in an MVC paradigm. It manages the model, + stored in the projectDictionary, and plotting data, and responds to enableNotifications + of changes. +=# +mutable struct Project + name::String + projectDirectory:: String + projectDictionary::Dict{String,Any} + plt::Any #For the plot + plotOptions::Int # = combinations of MODEL, GRID, MESH +# +# For drawing +# + outerBndryPoints ::Array{Any} # CHAIN + outerBndryNames ::Array{String} + innerBoundaryPoints ::Array{Any} # Array of CHAINs + innerBoundaryNames ::Array{Any} # Array of CHAINs of names + innerBoundaryChainNames::Array{String} # Array of the names of the CHAINs + refinementRegionPoints ::Array{Array{Float64,2}} # Array of Array of points + refinementRegionNames ::Array{String} + refinementRegionLoc ::Array{Array{Float64}} # Center point of a refinement region + bounds ::Array{Float64} + userBounds ::Array{Float64} + xGrid ::Array{Float64} + yGrid ::Array{Float64} + xMesh ::Array{Float64} + yMesh ::Array{Float64} + backgroundGridShouldUpdate::Bool + meshShouldUpdate ::Bool +end + +const defaultPlotPts = 200 +const meshFileFormats = Set(["ISM", "ISM-V2", "ABAQUS"]) +const plotFileFormats = Set(["sem", "skeleton"]) +const smootherTypes = Set(["LinearSpring", "LinearAndCrossbarSpring"]) +const statusValues = Set(["ON", "OFF"]) +const refinementTypes = Set(["smooth", "sharp"]) + + +""" + openProject(fileName::String, folder::String) + +Open existing project described in the control File. + + folder = folder the control file is in + fileName = the name of the file +""" +function openProject(fileName::String, folder::String) + + controlFile = joinpath(folder,fileName) + controlDict = ImportControlFile(controlFile) + + # Strip off the file path into a temporary name + tempName = splitdir(fileName) + # Separate the project name from `.control` + splitName = split(tempName[2], ".") + # Pull the correct project name + s = string(splitName[1]) # This is dumb + + # Instantiate an empty project with some defaults + proj = newProject(s,folder) +# +# Overwrite defaults +# + proj.projectDictionary = controlDict + + assemblePlotArrays(proj) + clearUndoRedo() + + return proj +end + + +""" + saveProject(proj::Project) + + proj = Project to be saved +Save a project dictionary to the file path specified when the project was created. +""" +function saveProject(proj::Project) + fileName = joinpath(proj.projectDirectory,proj.name)*".control" + WriteControlFile(proj.projectDictionary,fileName) +end + + +""" + newProject(name::String, folder::String) + +Create a new project with the given name. That name will be used +for the mesh and plot files in the specified folder. +""" +function newProject(name::String, folder::String) + ibChainPoints = Any[] + ibChainNames = String[] + ibNames = Any[] + obNames = String[] + obPnts = Any[] + projectDict = Dict{String,Any}() + plt = nothing + undoStack = Any[] + redoStack = Any[] + bounds = emptyBounds() # top, left, bottom, right + userBounds = emptyBounds() + xGrid = Float64[] + yGrid = Float64[] + xMesh = Float64[] + yMesh = Float64[] + refinementRegionPts = Array{Array{Float64,2}}[] + refinementRegionNames = Array{String}[] + refinementRegionLocs = Array{Array{Float64}}[] + plotOptions = 0 +# + proj = Project(name, folder, projectDict, plt, plotOptions, obPnts, obNames, + ibChainPoints,ibNames, ibChainNames, + refinementRegionPts,refinementRegionNames, refinementRegionLocs, + bounds, userBounds, xGrid, yGrid, xMesh, yMesh, + true, false) + + addObserver(proj,"CURVE_DID_CHANGE_NOTIFICATION",curveDidChange) + addObserver(proj,"MODEL_DID_CHANGE_NOTIFICATION",modelDidChange) + addObserver(proj,"BGRID_DID_CHANGE_NOTIFICATION",backgroundGridDidChange) + addObserver(proj,"MESH_WAS_GENERATED_NOTIFICATION",meshWasGenerated) + addObserver(proj,"MESH_WAS_DELETED_NOTIFICATION",meshWasDeleted) + addObserver(proj,"REFINEMENT_WAS_ADDED_NOTIFICATION",refinementWasAdded) + addObserver(proj,"REFINEMENT_WAS_CHANGED_NOTIFICATION",refinementDidChange) + enableNotifications() +# +# Set some default values +# + addRunParameters!(proj) + addSpringSmoother!(proj) + enableUndo() +# +# Create the output directory if it does not exist +# + if !isdir(abspath(normpath(folder))) + mkdir(abspath(normpath(folder))) + end + + return proj +end + + +""" + hasBackgroundGrid(proj::Project) + +Tests to see if the project has a backgroundGrid dictionary defined. +""" +function hasBackgroundGrid(proj::Project) + controlDict = getControlDict(proj) + if haskey(controlDict,"BACKGROUND_GRID") + return true + else + return false + end +end + + +function assemblePlotArrays(proj::Project) + + empty!(proj.outerBndryPoints) + empty!(proj.outerBndryNames) + empty!(proj.innerBoundaryChainNames) + empty!(proj.innerBoundaryPoints) + empty!(proj.innerBoundaryNames) + empty!(proj.xGrid) + empty!(proj.yGrid) + + bounds = emptyBounds() + + modelDict = getModelDict(proj) + + if haskey(modelDict,"OUTER_BOUNDARY") + outerBoundary = modelDict["OUTER_BOUNDARY"] + obChain = outerBoundary["LIST"] + proj.outerBndryPoints = chainPoints(obChain,defaultPlotPts) + + chB = chainBounds(proj.outerBndryPoints) + bounds = bboxUnion(bounds,chB) + + for crv in obChain + push!(proj.outerBndryNames,crv["name"]) + end + end + + if haskey(modelDict,"INNER_BOUNDARIES") + innerBoundaries = modelDict["INNER_BOUNDARIES"] + innerBoundaryList = innerBoundaries["LIST"] #LIST of CHAINS + for d in innerBoundaryList + push!(proj.innerBoundaryChainNames, d["name"]) + ibChain = d["LIST"] + ibPnts = chainPoints(ibChain,defaultPlotPts) + push!(proj.innerBoundaryPoints,ibPnts) + chB = chainBounds(ibPnts) + bounds = bboxUnion(bounds,chB) + names = String[] + for crv in ibChain + push!(names,crv["name"]) + end + push!(proj.innerBoundaryNames,names) + end + end + + controlDict = getControlDict(proj) + + if haskey(controlDict,"REFINEMENT_REGIONS") + refinementBlock = controlDict["REFINEMENT_REGIONS"] + refinementsList = refinementBlock["LIST"] + for ref in refinementsList + addRefinementRegionPoints!(proj,ref) + end + end + proj.bounds = bounds +end + + +function projectBounds(proj::Project) + bounds = emptyBounds() + + if !isempty(proj.outerBndryPoints) + chB = chainBounds(proj.outerBndryPoints) + bounds = bboxUnion(bounds,chB) + end + + if !isempty(proj.innerBoundaryPoints) + for i = 1:length(proj.innerBoundaryPoints) + ibPnts = proj.innerBoundaryPoints[i] + chB = chainBounds(ibPnts) + bounds = bboxUnion(bounds,chB) + end + end + return bounds +end + + +function projectGrid(proj::Project) + + controlDict = proj.projectDictionary["CONTROL_INPUT"] + + if haskey(controlDict,"BACKGROUND_GRID") + bgDict = controlDict["BACKGROUND_GRID"] + + if haskey(bgDict,"dx") + N = intArrayForKeyFromDictionary("N", bgDict) + x0 = realArrayForKeyFromDictionary("x0",bgDict) + left = x0[1] + bottom = x0[2] + xGrid = zeros(Float64, N[1]+1) + yGrid = zeros(Float64, N[2]+1) + dx = realArrayForKeyFromDictionary("dx", bgDict) + for i = 1:N[1]+1 + xGrid[i] = left + (i-1)*dx[1] + end + for j = 1:N[2]+1 + yGrid[j] = bottom + (j-1)*dx[2] + end + else + dx = realArrayForKeyFromDictionary("background grid size", bgDict) + bounds = proj.bounds + + width = bounds[RIGHT] - bounds[LEFT] + height = bounds[TOP] - bounds[BOTTOM] + + Nx = Int(round(width/dx[1])) + 3 # Want the model inside the grid + Ny = Int(round(height/dx[2])) + 3 + + xGrid = zeros(Float64, Nx) + yGrid = zeros(Float64, Ny) + + for i = 1:Nx + xGrid[i] = bounds[LEFT] + (i-2)*dx[1] # Arrays start at 1, ugh. + end + for j = 1:Ny + yGrid[j] = bounds[BOTTOM] + (j-2)*dx[2] + end + end + end + + return xGrid, yGrid +end + + +# +# NOTIFICATION ACTIONS +# +function curveDidChange(proj::Project,crv::Dict{String,Any}) + curveName = getCurveName(crv) +# +# Find the curve location: See if the curve is in the outer boundary +# + for (i,s) in enumerate(proj.outerBndryNames) + if s == curveName + proj.outerBndryPoints[i] = curvePoints(crv,defaultPlotPts) + if !isnothing(proj.plt) + options = proj.plotOptions + updatePlot!(proj, options) + end + return nothing + end + end +# +# Otherwise, see if it is an inner boundary +# + crvNumber, bndryNumber = innerBoundaryIndices(proj,curveName) + if crvNumber == 0 || bndryNumber == 0 + return nothing + end + innerBoundaryPoints = proj.innerBoundaryPoints[bndryNumber] + innerBoundaryPoints[crvNumber] = curvePoints(crv,defaultPlotPts) + proj.backgroundGridShouldUpdate = true + + if !isnothing(proj.plt) + options = proj.plotOptions + updatePlot!(proj, options) + end + return nothing +end + + +function modelDidChange(proj::Project, sender::Project) + + if proj === sender && !isnothing(proj.plt) + options = proj.plotOptions + if (options & MODEL) == 0 + options = options + MODEL + end + updatePlot!(proj, options) + end +end + + +function backgroundGridDidChange(proj::Project, sender::Project) + if proj === sender && !isnothing(proj.plt) + proj.backgroundGridShouldUpdate = true + options = proj.plotOptions + if (options & GRID) == 0 + options = options + GRID + end + updatePlot!(proj, options) + end +end + + +function refinementWasAdded(proj::Project, sender::Project) + if proj === sender && !isnothing(proj.plt) + options = proj.plotOptions + if (options & REFINEMENTS) == 0 + options = options + REFINEMENTS + end + updatePlot!(proj, options) + end +end + + +function refinementDidChange(proj::Project, sender::Dict{String,Any}) + regionName = sender["name"] + lst = getAllRefinementRegions(proj) + indx = 0 + for (i,r) in enumerate(lst) + if r["name"] == regionName + indx = i + break + end + end + + if indx > 0 + x = refinementRegionPoints(sender) + proj.refinementRegionPoints[indx] = x + proj.refinementRegionNames[indx] = sender["name"] + center = getRefinementRegionCenter(sender) + proj.refinementRegionLoc[indx] = center + + if !isnothing(proj.plt) + options = proj.plotOptions + if (options & REFINEMENTS) == 0 + options = options + REFINEMENTS + end + updatePlot!(proj, options) + end + end +end + + +function meshWasGenerated(proj::Project, sender::Project) + if proj === sender && !isnothing(proj.plt) + options = proj.plotOptions + options = (MODEL & options) + MESH + proj.meshShouldUpdate = true + updatePlot!(proj, options) + end +end + + +function meshWasDeleted(proj::Project, sender::Project) + if proj === sender && !isnothing(proj.plt) + options = proj.plotOptions + if (MESH & options) > 0 + options = options - MESH + end + proj.meshShouldUpdate = false + updatePlot!(proj, options) + end +end + + +""" + modelCurvesAreOK(proj::Project) + +Go through all curves in the model and make sure they are connected and closed. +Also, remove any empty outer / inner boundary chains. + +Returns true if all curves are connected and closed, false otherwise. +""" +function modelCurvesAreOK(proj::Project) + result = true + if !haskey(proj.projectDictionary,"MODEL") + return true + end + modelDict = getModelDict(proj) + if haskey(modelDict,"OUTER_BOUNDARY") + chain = getOuterBoundaryChainList(proj) + if !isempty(chain) + result = modelChainIsOK(chain,"Outer") + else + delete!(modelDict,"OUTER_BOUNDARY") + end + end + if haskey(modelDict,"INNER_BOUNDARIES") + innerBoundariesList = getAllInnerBoundaries(proj) + if isempty(innerBoundariesList) + delete!(modelDict,"INNER_BOUNDARIES") + return result + end + for chain in innerBoundariesList + chainName = string(chain["name"]) + chainList = chain["LIST"] + result = result && modelChainIsOK(chainList,chainName) + end + end + return result +end + + +""" + modelChainIsOK(chain::Vector{Dict{String, Any}}, chainName::String) + +Returns true if the chain of curves is contiguous and closed; false otherwise. +""" +function modelChainIsOK(chain::Vector{Dict{String, Any}}, chainName::String) + result = true + for i in 1:length(chain)-1 + crv1 = chain[i] + crv2 = chain[i+1] + if !curvesMeet(crv1,crv2) + name1 = getCurveName(crv1) + name2 = getCurveName(crv2) + @warn "The curve $name2 does not meet the previous curve, $name1." + result = false + end + end + crv1 = last(chain) + crv2 = chain[1] + if !curvesMeet(crv1,crv2) + name1 = getCurveName(crv1) + name2 = getCurveName(crv2) + @warn "The boundary curve $chainName is not closed. Fix to generate mesh" + result = false + end + + return result +end diff --git a/src/Project/RefinementRegionsAPI.jl b/src/Project/RefinementRegionsAPI.jl new file mode 100644 index 00000000..2d23a474 --- /dev/null +++ b/src/Project/RefinementRegionsAPI.jl @@ -0,0 +1,443 @@ + +""" + newRefinementCenter(name, type, + center, meshSize, + width) + +Create refinement center of `type` "smooth" or "sharp" centered at `center = [x,y,z]`` +with a mesh size `meshSize` spread over a radius `width`. +""" +function newRefinementCenter(name::String, type::String, + x0::Array{Float64}, h::Float64, + w::Float64) + disableUndo() + disableNotifications() + centerDict = Dict{String,Any}() + centerDict["TYPE"] = "REFINEMENT_CENTER" + setRefinementType!(centerDict,type) + setRefinementLocation!(centerDict,x0) + setRefinementGridSize!(centerDict,h) + setRefinementWidth!(centerDict,w) + setRefinementName!(centerDict,name) + enableNotifications() + enableUndo() + return centerDict +end + + +""" + addRefinementRegion!(proj::Project,r::Dict{String,Any}) + +Add the refinement region to the project +""" +function addRefinementRegion!(proj::Project,r::Dict{String,Any}) + lst = getListInControlDictNamed(proj,"REFINEMENT_REGIONS") + push!(lst,r) + addRefinementRegionPoints!(proj,r) + enableUndo() + registerWithUndoManager(proj,removeRefinementRegion!, (r["name"],), "Add Refinement Region") + enableNotifications() + postNotificationWithName(proj,"REFINEMENT_WAS_ADDED_NOTIFICATION",(nothing,)) +end + + +""" + addRefinementRegionPoints!(proj::Project, r::Dict{String,Any}) + +Compute and add to the project the plotting points for the refinement region +""" +function addRefinementRegionPoints!(proj::Project, r::Dict{String,Any}) + + x = refinementRegionPoints(r) + push!(proj.refinementRegionPoints,x) + push!(proj.refinementRegionNames, r["name"]) + center = getRefinementRegionCenter(r) + push!(proj.refinementRegionLoc,center) +end + + +""" + refinementRegionPoints(r::Dict{String,Any}) + +Returns Array{Float64,2} being the plotting points of a refinement region +""" +function refinementRegionPoints(r::Dict{String,Any}) + + if r["TYPE"] == "REFINEMENT_CENTER" + center = getRefinementLocation(r) + radius = getRefinementWidth(r) + + N = defaultPlotPts + x = zeros(Float64,N+1,2) + t = zeros(Float64,N+1) + for i = 1:N+1 + t[i] = (i-1)/N + end + arcCurvePoints(center,radius,0.0,360.0,"degrees",t,x) + return x + else + xStart = realArrayForKeyFromDictionary("x0",r) + xEnd = realArrayForKeyFromDictionary("x1",r) + dx = xEnd - xStart + l = sqrt(dx[1]^2 + dx[2]^2) + w = realForKeyFromDictionary("w",r) + v = [-dx[2]/l,dx[1]/l] + x1 = xStart[1:2] + w*v + x2 = xEnd[1:2] + w*v + v = [dx[2]/l,-dx[1]/l] + x3 = xEnd[1:2] + w*v + x4 = xStart[1:2] + w*v + x = zeros(Float64,5,2) + x[1,:] = x1 + x[2,:] = x2 + x[3,:] = x3 + x[4,:] = x4 + x[5,:] = x1 + return x + end + +end + + +""" + getRefinementRegionCenter(r::Dict{String,Any}) + +Get, or compute, the center of the given refinement region. +""" +function getRefinementRegionCenter(r::Dict{String,Any}) + if r["TYPE"] == "REFINEMENT_CENTER" + center = getRefinementLocation(r) + return center[1:2] + else + xStart = realArrayForKeyFromDictionary("x0",r) + xEnd = realArrayForKeyFromDictionary("x1",r) + xAvg = 0.5*(xStart + xEnd) + return xAvg[1:2] + end +end + + +""" + removeRefinementRegion!(proj::Project, name::String) + +Delete the named refinement region. +""" +function removeRefinementRegion!(proj::Project, name::String) + i,r = getRefinementRegion(proj,name) + lst = getAllRefinementRegions(proj) + deleteat!(lst,i) + deleteat!(proj.refinementRegionLoc,i) + deleteat!(proj.refinementRegionNames,i) + deleteat!(proj.refinementRegionPoints,i) + registerWithUndoManager(proj,insertRefinementRegion!, (r,i,), "Remove Refinement Region") + enableNotifications() + postNotificationWithName(proj,"REFINEMENT_WAS_ADDED_NOTIFICATION",(nothing,)) +end + + +""" + insertRefinementRegion!(proj::Project, r::Dict{String,Any}, indx::Int) + +Used by undo() +""" +function insertRefinementRegion!(proj::Project, r::Dict{String,Any}, indx::Int) + lst = getAllRefinementRegions(proj) + registerWithUndoManager(proj,removeRefinementRegion!, (r["name"],), "Set Insert Refinement Region") + insert!(lst,indx,r) + x = refinementRegionPoints(r) + insert!(proj.refinementRegionPoints,indx,x) + center = getRefinementRegionCenter(r) + insert!(proj.refinementRegionLoc,indx,center) + insert!(proj.refinementRegionNames,indx,r["name"]) + postNotificationWithName(proj,"REFINEMENT_WAS_ADDED_NOTIFICATION",(nothing,)) +end + + +""" + newRefinementLine(name, type, + start, end, + meshSize, + width) + +Create refinement line of type "smooth" or "sharp" between `start` = [x,y,z] and `end` = [x,y,z] +with a mesh size `meshSize` spread over a width `width`. +""" +function newRefinementLine(name::String, type::String, + x0::Array{Float64}, x1::Array{Float64}, + h::Float64, + w::Float64) + disableUndo() + disableNotifications() + lineDict = Dict{String,Any}() + lineDict["TYPE"] = "REFINEMENT_LINE" + setRefinementType!(lineDict,type) + setRefinementStart!(lineDict,x0) + setRefinementEnd!(lineDict,x1) + setRefinementGridSize!(lineDict,h) + setRefinementWidth!(lineDict,w) + setRefinementName!(lineDict,name) + enableNotifications() + enableUndo() + return lineDict +end + + +""" + getRefinementRegion(proj::Project, indx) + +Get the refinement region with index, indx from the project. Returns nothing if +there is none. The return value is a dictionary that represents the refinement region. +""" +function getRefinementRegion(proj::Project, indx::Int) + lst = getListInControlDictNamed(proj,"REFINEMENT_REGIONS") + if indx > length(lst) + error("Index ",indx," is larger than the number of refinement regions ", length(lst)) + end + return lst[indx] +end + + +""" + getAllRefinementRegions(proj::Project) + +Get the list of refinement regions. +""" +function getAllRefinementRegions(proj::Project) + lst = getListInControlDictNamed(proj,"REFINEMENT_REGIONS") + return lst +end + + +""" + (i,r) = getRefinementRegion(project, name) + +Get the refinement region with the given name and its location in the list of refinement regions. +""" +function getRefinementRegion(proj::Project, name::String) + lst = getListInControlDictNamed(proj,"REFINEMENT_REGIONS") + for (i,r) in enumerate(lst) + if r["name"] == name + return i,r + end + end + error("Refinement region with name ", name, " not found!") +end + + +""" + setRefinementType!(refinementRegion, type) + +Set the type, either "smooth" or "sharp" for the given refinement region. +""" +function setRefinementType!(r::Dict{String,Any}, type::String) + if !in(type,refinementTypes) + @warn "Acceptable refinement types are `smooth` and `sharp`. Try again." + return + end + + if haskey(r,"type") + oldType = r["type"] + registerWithUndoManager(r,setRefinementType!, (oldType,), "Set Refinement Type") + end + r["type"] = type +end + + +""" + getRefinementType(r::Dict{String,Any}) + +Return the type of refinement, either "smooth" or "sharp". `r` is the dictionary that +represents the refinement region. +""" +function getRefinementType(r::Dict{String,Any}) + return r["type"] +end + + +""" + setRefinementName!(r::Dict{String,Any}, type) + +Set a name for the refinement region.`r` is the dictionary that +represents the refinement region. +""" +function setRefinementName!(r::Dict{String,Any}, name::String) + if haskey(r,"name") + oldName = r["name"] + registerWithUndoManager(r,setRefinementName!, (oldName,), "Set Refinement Name") + end + r["name"] = name + postNotificationWithName(r,"REFINEMENT_WAS_CHANGED_NOTIFICATION",(nothing,)) +end + + +""" + getRefinementName(r::Dict{String,Any}) + +Return name of the refinement. `r` is the dictionary that represents the refinement region. +""" +function getRefinementName(r::Dict{String,Any}) + return r["name"] +end + + +""" + setRefinementLocation!(refinementCenter, location) + +Set the location of a refinement center to location = [x,y,z]. +""" +function setRefinementLocation!(r::Dict{String,Any}, x::Array{Float64}) + x0Str = "[$(x[1]),$(x[2]),$(x[3])]" + setRefinementLocation!(r,x0Str) + return nothing +end + + +function setRefinementLocation!(r::Dict{String,Any}, x0Str::String) + if haskey(r,"x0") + old = r["x0"] + registerWithUndoManager(r,setRefinementLocation!, (old,), "Set Refinement Center") + end + r["x0"] = x0Str + postNotificationWithName(r,"REFINEMENT_WAS_CHANGED_NOTIFICATION",(nothing,)) + return nothing +end + + +""" + getRefinementLocation(r::Dict{String,Any}) + +Return Array{Float64} of the location of the refinement center.`r` is the dictionary that +represents the refinement region. +""" +function getRefinementLocation(r::Dict{String,Any}) + return realArrayForKeyFromDictionary("x0",r) +end + + +""" + setRefinementGridSize!(r::Dict{String,Any}, h) + +Set the grid size, `h` for the refinement region. `r` is the dictionary that +represents the refinement region. +""" +function setRefinementGridSize!(r::Dict{String,Any}, h::Float64) + if haskey(r,"h") + old = r["h"] + registerWithUndoManager(r,setRefinementGridSize!, (old,), "Set Refinement Grid Size") + end + r["h"] = string(h) + postNotificationWithName(r,"REFINEMENT_WAS_CHANGED_NOTIFICATION",(nothing,)) +end + + +function setRefinementGridSize!(r::Dict{String,Any}, h::String) + hf = parse(Float64,h) + setRefinementGridSize!(r,hf) +end + + +""" + getRefinementGridSize(r::Dict{String,Any}) + +Returns the grid size,h, as Float64. `r` is the dictionary that +represents the refinement region. +""" +function getRefinementGridSize(r::Dict{String,Any}) + return parse(Float64,r["h"]) +end + + +""" + setRefinementWidth!(r::Dict{String,Any}, width) + +Set the width of the refinement region. `r` is the dictionary that +represents the refinement region. +""" +function setRefinementWidth!(r::Dict{String,Any},w::Float64) + if haskey(r,"w") + old = r["w"] + registerWithUndoManager(r,setRefinementWidth!, (old,), "Set Refinement Width") + end + r["w"] = string(w) + postNotificationWithName(r,"REFINEMENT_WAS_CHANGED_NOTIFICATION",(nothing,)) +end + + +function setRefinementWidth!(r::Dict{String,Any},w::String) + wf = parse(Float64,w) + setRefinementWidth!(r,wf) +end +# +# -------------------------------------------------------------------------------------- +# +""" + getRefinementWidth(r::Dict{String,Any}) + +Returns the region width,w, as Float64. `r` is the dictionary that +represents the refinement region. +""" +function getRefinementWidth(r::Dict{String,Any}) + return parse(Float64,r["w"]) +end + + +""" + setRefinementStart!(refinementRegion, location) + +Set the start point location of a refinement line, `location = [x, y, z]`. +""" +function setRefinementStart!(r::Dict{String,Any}, x::Array{Float64}) + x0Str = "[$(x[1]),$(x[2]),$(x[3])]" + setRefinementStart!(r,x0Str) +end + + +function setRefinementStart!(r::Dict{String,Any}, x0Str::String) + if haskey(r,"x0") + old = r["x0"] + registerWithUndoManager(r,setRefinementStart!, (old,), "Set Refinement Start") + end + r["x0"] = x0Str + postNotificationWithName(r,"REFINEMENT_WAS_CHANGED_NOTIFICATION",(nothing,)) +end + + +""" + getRefinementStart (r::Dict{String,Any}) + +Return Array{Float64} of the start location of the refinement line. `r` is the dictionary that +represents the refinement region. +""" +function getRefinementStart(r::Dict{String,Any}) + return realArrayForKeyFromDictionary("x0",r) +end + + +""" + setRefinementEnd!(refinementRegion, location) + +Set the end point location of a refinement line, `location = [x, y, z]`. +""" +function setRefinementEnd!(r::Dict{String,Any}, x::Array{Float64}) + x0Str = "[$(x[1]),$(x[2]),$(x[3])]" + setRefinementEnd!(r,x0Str) +end + +function setRefinementEnd!(r::Dict{String,Any}, x0Str::String) + if haskey(r,"x1") + old = r["x1"] + registerWithUndoManager(r,setRefinementEnd!, (old,), "Set Refinement End") + end + r["x1"] = x0Str + postNotificationWithName(r,"REFINEMENT_WAS_CHANGED_NOTIFICATION",(nothing,)) +end + + +""" + getRefinementEnd(r::Dict{String,Any}) + +Return Array{Float64} of the end location of the refinement line +""" +function getRefinementEnd(r::Dict{String,Any}) + return realArrayForKeyFromDictionary("x1",r) +end diff --git a/src/Project/RunParametersAPI.jl b/src/Project/RunParametersAPI.jl new file mode 100644 index 00000000..862c82fa --- /dev/null +++ b/src/Project/RunParametersAPI.jl @@ -0,0 +1,187 @@ + +""" + addRunParameters!(proj::Project, + plotFormat::String = "skeleton", + meshFileFormat::String = "ISM-V2", + polynomialOrder::Int = 5) + +Add a RUN_PARAMETERS block and set all the parameters in one call. +""" +function addRunParameters!(proj::Project, + plotFormat::String = "skeleton", + meshFileFormat::String = "ISM-V2", + polynomialOrder::Int = 5) + + setPlotFileFormat!(proj, plotFormat) + setMeshFileFormat!(proj, meshFileFormat) + setPolynomialOrder!(proj, polynomialOrder) + + rpDict = getDictInControlDictNamed(proj,"RUN_PARAMETERS") + registerWithUndoManager(proj,removeRunParameters!, (nothing,), "Add Run Parameters") + + return rpDict +end + + +""" + removeRunParameters!(proj::Project) + +Remove the run parameters block from the project. This is not undo-able. +""" +function removeRunParameters!(proj::Project) + cDict = getControlDict(proj) + if haskey(cDict,"RUN_PARAMETERS") + delete!(cDict,"RUN_PARAMETERS") + end +end + + +""" + setName(proj::Project,name::String) + +The `name` of the project is the filename to be used by the mesh, plot, and +stats files. It is also the name of the control file the tool will produce. +""" +function setName!(proj::Project, name::String) + + oldName = proj.name + registerWithUndoManager(proj,setName!,(oldName,),"Set Project Name") + proj.name = name + setFileNames!(proj, getMeshFileFormat(proj)) +end + + +""" + getName(proj::Project) + +Returns the filename to be used by the mesh, plot, control, and +stats files. +""" +function getName(proj::Project) + return proj.name +end + + +""" + setPolynomialOrder(proj::Project, p::Int) + +Set the polynomial order for boundary curves in the mesh file to `p`. +""" +function setPolynomialOrder!(proj::Project, p::Int) + key = "polynomial order" + rpDict = getDictInControlDictNamed(proj,"RUN_PARAMETERS") + if haskey(rpDict,key) + oldP = parse(Int,rpDict[key]) + registerWithUndoManager(proj,setPolynomialOrder!,(oldP,),"Set Order") + end + rpDict["polynomial order"] = string(p) +end + + +""" + getPolynomialOrder(proj::Project) + +Returns the polynomial order for boundary curves in the mesh file. +""" +function getPolynomialOrder(proj::Project) + rpDict = getDictInControlDictNamed(proj,"RUN_PARAMETERS") + return parse(Int,rpDict["polynomial order"]) +end + + +""" + setMeshFileFormat(proj::Project, meshFileFormat::String) + +Set the file format for the mesh file. Acceptable choices +are "ISM", "ISM-V2", or "ABAQUS". +""" +function setMeshFileFormat!(proj::Project, meshFileFormat::String) + if !in(meshFileFormat,meshFileFormats) + @warn "Acceptable file formats are: `ISM-V2`, `ISM`, or `ABAQUS`. Try again." + return + end + key = "mesh file format" + rpDict = getDictInControlDictNamed(proj,"RUN_PARAMETERS") + if haskey(rpDict,key) + oldFormat = rpDict[key] + registerWithUndoManager(proj,setMeshFileFormat!,(oldFormat,),"Set Mesh Format") + end + rpDict[key] = meshFileFormat + + # Set the appropriate file names and extensions from the given `meshFileFormat` + setFileNames!(proj, meshFileFormat) +end + + +""" + getMeshFileFormat(proj::Project) + +Returns the format in which the mesh will be written. +""" +function getMeshFileFormat(proj::Project) + rpDict = getDictInControlDictNamed(proj,"RUN_PARAMETERS") + return rpDict["mesh file format"] +end + + +""" + setPlotFileFormat(proj::Project, plotFileFormat::String) + +Set the file format for the plot file. Acceptable choices +are "sem", which includes interior nodes and boundary nodes and "skeleton", which includes +only the corner nodes. +""" +function setPlotFileFormat!(proj::Project, plotFileFormat::String) + if !in(plotFileFormat,plotFileFormats) + @warn "Acceptable plot formats are: `sem` or `skeleton`. Try again." + return + end + rpDict = getDictInControlDictNamed(proj,"RUN_PARAMETERS") + key = "plot file format" + if haskey(rpDict,key) + oldFormat = rpDict[key] + registerWithUndoManager(proj,setPlotFileFormat!,(oldFormat,),"Set Plot Format") + end + rpDict[key] = plotFileFormat +end + + +""" + getPlotFileFormat(proj::Project) + +Returns the plot file format. +""" +function getPlotFileFormat(proj::Project) + rpDict = getDictInControlDictNamed(proj,"RUN_PARAMETERS") + return rpDict["plot file format"] +end + + +function setFileNames!(proj::Project, meshFileFormat::String) + rpDict = getDictInControlDictNamed(proj,"RUN_PARAMETERS") + if meshFileFormat == "ABAQUS" + rpDict["mesh file name"] = joinpath(proj.projectDirectory, proj.name *".inp") + else + rpDict["mesh file name"] = joinpath(proj.projectDirectory, proj.name *".mesh") + end + rpDict["plot file name"] = joinpath(proj.projectDirectory, proj.name *".tec") + rpDict["stats file name"] = joinpath(proj.projectDirectory, proj.name *".txt") + end + + + function getMeshFileName(proj::Project) + rpDict = getDictInControlDictNamed(proj,"RUN_PARAMETERS") + return rpDict["mesh file name"] + end + + + function getPlotFileName(proj::Project) + rpDict = getDictInControlDictNamed(proj,"RUN_PARAMETERS") + return rpDict["plot file name"] + end + + + function getStatsFileName(proj::Project) + rpDict = getDictInControlDictNamed(proj,"RUN_PARAMETERS") + return rpDict["stats file name"] + end diff --git a/src/Project/SmootherAPI.jl b/src/Project/SmootherAPI.jl new file mode 100644 index 00000000..50e1aa4a --- /dev/null +++ b/src/Project/SmootherAPI.jl @@ -0,0 +1,107 @@ + +""" + addSpringSmoother!(status::String, type::String, nIterations::Int) + +Status is either `ON` or `OFF` +Type is either `LinearSpring` or `LinearAndCrossbarSpring` +""" +function addSpringSmoother!(proj::Project,status::String = "ON", + type::String = "LinearAndCrossbarSpring", + nIterations::Int = 25) + if !in(status,statusValues) + @warn "Acceptable smoother status are: `ON` or `OFF`. Try again." + return + end + if !in(type,smootherTypes) + @warn "Acceptable smoothers are: `LinearAndCrossbarSpring` or `LinearSpring`. Try again." + return + end + setSmoothingStatus!(proj,status) + setSmoothingType!(proj,type) + setSmoothingIterations!(proj,nIterations) +end + + +""" + setSmoothingStatus(proj:Project, status::String) + +Status is either "ON" or "OFF" +""" +function setSmoothingStatus!(proj::Project, status::String) + if !in(status,statusValues) + @warn "Acceptable smoother status is either: `ON` or `OFF`. Try again." + return + end + smDict = getDictInControlDictNamed(proj,"SPRING_SMOOTHER") + smDict["smoothing"] = status +end + + +""" + smoothingStatus(proj::Project) + +Returns whether the smoother will be "ON" or "OFF" +""" +function getSmoothingStatus(proj::Project) + smDict = getDictInControlDictNamed(proj,"SPRING_SMOOTHER") + return smDict["smoothing"] +end + + +""" + setSmoothingType!(proj:Project, status::String) + +Type is either `LinearSpring` or `LinearAndCrossbarSpring` +""" +function setSmoothingType!(proj::Project, type::String) + if !in(type,smootherTypes) + @warn "Acceptable smoothers are: `LinearAndCrossbarSpring` or `LinearSpring`. Try again." + return + end + smDict = getDictInControlDictNamed(proj,"SPRING_SMOOTHER") + smDict["smoothing type"] = type +end + + +""" + getSmoothingType(proj::Project) + +Returns either "LinearSpring" or "LinearAndCrossbarSpring" +""" +function getSmoothingType(proj::Project) + smDict = getDictInControlDictNamed(proj,"SPRING_SMOOTHER") + return smDict["smoothing type"] +end + + +""" + setSmoothingIterations!(proj::Project, iterations::Int) + +Set the number of iterations to smooth the mesh. +""" +function setSmoothingIterations!(proj::Project, iterations::Int) + smDict = getDictInControlDictNamed(proj,"SPRING_SMOOTHER") + smDict["number of iterations"] = iterations +end + + +""" + getSmoothingIterations(proj::Project) + +Get the number of iterations to smooth the mesh. +""" +function getSmoothingIterations(proj::Project) + smDict = getDictInControlDictNamed(proj,"SPRING_SMOOTHER") + return smDict["number of iterations"] +end + + +""" + removeSpringSmoother!(proj::Project) + +Remove the background grid block from the project. +""" +function removeSpringSmoother!(proj::Project) + cDict = getControlDict(proj) + delete!(cDict,"SPRING_SMOOTHER") +end diff --git a/src/Project/Undo.jl b/src/Project/Undo.jl new file mode 100644 index 00000000..b5489467 --- /dev/null +++ b/src/Project/Undo.jl @@ -0,0 +1,127 @@ + +struct UROperation + object::Any + action::Any + data ::Tuple + name ::String +end + +@enum UNDO_OPERATION_TYPE begin + UNDO_USER_OPERATION = 0 + UNDO_OPERATION = 1 + REDO_OPERATION = 2 + UNDO_IGNORE = 3 +end + +#= +TODO: The undo framework currently works globally, within the REPL. It *should* work project-by-project. +Note that these projects refer to the interactive mesh functionality projects, +not Julia projects in the sense of `Project.toml`. +To make the undo framework project based, undo() would be replaced by undo(project) and an .undoStack +property of the project would replace HQMglobalUndoStack. This is +not a big deal except if multiple projects are open, and muliple objects like curves have been +defined but not added to a project. In interactive mode curves are separate from projects until +added. (The same curve could be added to multiple projects.) So some logic needs to be +figured out before modifying below. If only one project is managed per session, +then this is not a problem. +=# +HQMglobalUndoStack = [] +HQMglobalRedoStack = [] +HQMglobalChangeOP = UNDO_IGNORE + + +function undo() + if !isempty(HQMglobalUndoStack) + op = pop!(HQMglobalUndoStack) + f = op.action + d = op.data + obj = op.object + global HQMglobalChangeOP = UNDO_OPERATION + if isnothing(d[1]) + f(obj) + else + f(obj,d...) + end + global HQMglobalChangeOP = UNDO_USER_OPERATION + return "Undo "*op.name + end + return "Empty undo stack. No action performed." +end + + +function redo() + if !isempty(HQMglobalRedoStack) + op = pop!(HQMglobalRedoStack) + f = op.action + d = op.data + obj = op.object + global HQMglobalChangeOP = REDO_OPERATION + if isnothing(d[1]) + f(obj) + else + f(obj,d...) + end + global HQMglobalChangeOP = UNDO_USER_OPERATION + return "Redo " * op.name + end + return "Empty redo stack. No action performed." +end + + +function registerUndo(obj, action, data::Tuple, name::String) + uOp = UROperation(obj,action,data,name) + push!(HQMglobalUndoStack,uOp) +end + + +function registerWithUndoManager(obj, action, oldData::Tuple, name::String) + + if HQMglobalChangeOP == UNDO_USER_OPERATION #User action + registerUndo(obj,action,oldData,name) + elseif HQMglobalChangeOP == UNDO_OPERATION #Undo operation + registerRedo(obj,action,oldData,name) + elseif HQMglobalChangeOP == REDO_OPERATION #Redo operation + registerUndo(obj,action,oldData,name) + else + # UNDO_IGNORE + end +end + +function registerRedo(obj, action, data::Tuple, name::String) + rOp = UROperation(obj,action,data,name) + push!(HQMglobalRedoStack,rOp) +end + + +function clearUndoRedo() + empty!(HQMglobalUndoStack) + empty!(HQMglobalRedoStack) +end + + +function undoActionName() + if !isempty(HQMglobalUndoStack) + op = last(HQMglobalUndoStack) + return op.name + end + return "No undo action in queue" +end + + +function redoActionName() + if !isempty(HQMglobalRedoStack) + op = last(HQMglobalRedoStack) + return op.name + end + return "No redo action in queue" +end + + +function disableUndo() + global HQMglobalChangeOP = UNDO_IGNORE +end + + +function enableUndo() + global HQMglobalChangeOP = UNDO_USER_OPERATION +end diff --git a/src/Viz/VizMesh.jl b/src/Viz/VizMesh.jl new file mode 100644 index 00000000..9c53ddeb --- /dev/null +++ b/src/Viz/VizMesh.jl @@ -0,0 +1,207 @@ + +function getMeshFromMeshFile(meshFile::AbstractString, meshFileFormat::AbstractString) + + if meshFileFormat == "ISM-V2" + open(meshFile,"r") do f + line = strip(readline(f)) # Header Should be ISM-V2 + line = readline(f) # Numbers of nodes, edges ... + values = split(line) + + nNodes = parse(Int64, values[1]) + nEdges = parse(Int64, values[2]) +# +# Read the nodes +# + nodes = zeros(Float64, nNodes, 2) + for i = 1:nNodes + values = split(readline(f)) + for j = 1:2 + nodes[i,j] = parse(Float64, values[j]) + end + end +# +# Read the edges and construct the lines array +# + xMesh = zeros(Float64, 3*nEdges) + yMesh = zeros(Float64, 3*nEdges) + + for i = 1:3:3*nEdges + + values = split(readline(f)) + n = parse(Int64,values[1]) + m = parse(Int64,values[2]) + + xMesh[i] = nodes[n,1] + xMesh[i+1] = nodes[m,1] + xMesh[i+2] = NaN + + yMesh[i] = nodes[n,2] + yMesh[i+1] = nodes[m,2] + yMesh[i+2] = NaN + + end + return xMesh, yMesh + end + elseif meshFileFormat == "ISM" + open(meshFile, "r") do f + # There is no header + line = readline(f) # Numbers of corners, elements and boundary polynomial order + values = split(line) + + nNodes = parse(Int64, values[1]) + nElements = parse(Int64, values[2]) + nBndy = parse(Int64, values[3]) +# +# Read the nodes +# + nodes = zeros(Float64, nNodes, 2) + for i = 1:nNodes + values = split(readline(f)) + for j = 1:2 + nodes[i,j] = parse(Float64, values[j]) + end + end +# +# Read the element ids (and skip all the boundary information) +# + elements = zeros(Int64,nElements,4) + temp = zeros(Int64, 4) + for i = 1:nElements + values = split(readline(f)) + for j = 1:4 + elements[i,j] = parse(Int64, values[j]) + end + values = split(readline(f)) + for j = 1:4 + temp[j] = parse(Int64, values[j]) + end + if sum(temp) == 0 + # straight-sided edge so just skip the boundary labels + readline(f) + else # sum(temp) > 0 + # At least one curved edge, so skip any boundary polynomial(s) and the labels + for j = 1:sum(temp) + for i = 1:nBndy+1 + readline(f) + end + end + readline(f) + end + end + # convenience mapping for element index corners + p = [[1 2 4 1] + [2 3 3 4]] + # Build the edges. This is only for plotting purposes so we might have some + # repeated edges + edge_id = 0 + edges = Dict{Int64, Any}() + for j in 1:nElements + for k in 1:4 + id1 = elements[j , p[1,k]] + id2 = elements[j , p[2,k]] + edge_id += 1 + push!(edges, edge_id => [id1 id2]) + end # k + end # j + # set the total number of edges + nEdges = edge_id + # use the edge information and pull the corner node physical values + xMesh = zeros(Float64, 3*nEdges) + yMesh = zeros(Float64, 3*nEdges) + edge_id = 0 + for i = 1:3:3*nEdges + edge_id += 1 + current_edge = edges[edge_id] + n = current_edge[1] + m = current_edge[2] + + xMesh[i] = nodes[n,1] + xMesh[i+1] = nodes[m,1] + xMesh[i+2] = NaN + + yMesh[i] = nodes[n,2] + yMesh[i+1] = nodes[m,2] + yMesh[i+2] = NaN + end + return xMesh, yMesh + end + elseif meshFileFormat == "ABAQUS" + # read in the entire file + file_lines = readlines(open(meshFile)) + # obtain the number of corners and elements in a circuitous way due to the ABAQUS format + # number of corner nodes + file_idx = findfirst(contains("*ELEMENT"), file_lines) - 1 + current_line = split(file_lines[file_idx], ",") + nNodes = parse(Int64, current_line[1]) + # number of elements + file_idx = findfirst(contains("** ***** HOHQMesh boundary information ***** **"), file_lines) - 1 + current_line = split(file_lines[file_idx], ",") + nElements = parse(Int64, current_line[1]) +# +# Read in the nodes +# + nodes = zeros(Float64, nNodes, 2) + file_idx = 4 + for i in 1:nNodes + current_line = split(file_lines[file_idx], ",") + for j = 2:3 + nodes[i, j-1] = parse(Float64, current_line[j]) + end + file_idx += 1 + end # i +# +# Read the element ids (and skip all the boundary information) +# + elements = zeros(Int64, nElements, 4) + # eat the element header + file_idx += 1 + for i = 1:nElements + current_line = split(file_lines[file_idx], ",") + for j = 2:5 + elements[i,j-1] = parse(Int64, current_line[j]) + end + file_idx += 1 + end + # convenience mapping for element index corners + p = [[1 2 4 1] + [2 3 3 4]] + # Build the edges. This is only for plotting purposes so we might have some + # repeated edges + edge_id = 0 + edges = Dict{Int64, Any}() + for j in 1:nElements + for k in 1:4 + id1 = elements[j , p[1,k]] + id2 = elements[j , p[2,k]] + edge_id += 1 + push!(edges, edge_id => [id1 id2]) + end # k + end # j + # set the total number of edges + nEdges = edge_id + # use the edge information and pull the corner node physical values + xMesh = zeros(Float64, 3*nEdges) + yMesh = zeros(Float64, 3*nEdges) + edge_id = 0 + for i = 1:3:3*nEdges + edge_id += 1 + current_edge = edges[edge_id] + n = current_edge[1] + m = current_edge[2] + + xMesh[i] = nodes[n,1] + xMesh[i+1] = nodes[m,1] + xMesh[i+2] = NaN + + yMesh[i] = nodes[n,2] + yMesh[i+1] = nodes[m,2] + yMesh[i+2] = NaN + end + return xMesh, yMesh + end +end + + +function plotMesh(plt, xMesh::Array{Float64}, yMesh::Array{Float64}) + lines!(plt[1,1], xMesh,yMesh) +end diff --git a/src/Viz/VizProject.jl b/src/Viz/VizProject.jl new file mode 100644 index 00000000..0d87c352 --- /dev/null +++ b/src/Viz/VizProject.jl @@ -0,0 +1,186 @@ + +const MODEL = 1; const GRID = 2; const MESH = 4; const EMPTY = 0 +const REFINEMENTS = 8; const ALL = 15 + + +""" + plotProject!(proj::Project, plotOptions::Int = 0) + +Plot objects specified by the `plotOptions`. Construct the `plotOptions` by the sum +of what is to be drawn from the choices `MODEL`, `GRID`, `MESH`, `REFINEMENTS`. + +Example: To plot the model and the grid, `plotOptions = MODEL + GRID`. To plot +just the mesh, `plotOptions = MESH`. + +To plot everything, `plotOptions = MODEL + GRID + MESH + REFINEMENTS` + +Contents are overlayed in the order: GRID, MESH, MODEL, REFINEMENTS +""" +function plotProject!(proj::Project, plotOptions::Int = 0) + + if isnothing(proj.plt) + proj.plt = Figure(resolution = (1000, 1000)) + end + plt = proj.plt + ax = plt[1,1] = Axis(plt) + + plotTheModel = ((plotOptions & MODEL) != 0) + plotTheGrid = ((plotOptions & GRID) != 0) + plotTheMesh = ((plotOptions & MESH) != 0) + plotTheRefinements = ((plotOptions & REFINEMENTS) != 0) + proj.plotOptions = plotOptions +# +# Plot the grid +# + if plotTheGrid && hasBackgroundGrid(proj) + if proj.backgroundGridShouldUpdate # Lazy evaluation of the background grid + proj.bounds = projectBounds(proj) + proj.xGrid, proj.yGrid = projectGrid(proj) + proj.backgroundGridShouldUpdate = false + end + nX = length(proj.xGrid) + nY = length(proj.yGrid) + z = zeros(Float64,nX,nY) + wireframe!(plt[1,1],proj.xGrid,proj.yGrid,z) + end +# +# Plot the mesh +# + if plotTheMesh + # Lazy creation of mesh plotting arrays + if proj.meshShouldUpdate || (isempty(proj.xMesh) && isempty(proj.yMesh)) + meshFileName = getMeshFileName(proj) + if isfile(meshFileName) + fileFormat = getMeshFileFormat(proj) + proj.xMesh, proj.yMesh = getMeshFromMeshFile(meshFileName, fileFormat) + plotMesh(plt, proj.xMesh, proj.yMesh) + end + proj.meshShouldUpdate = false + else + plotMesh(plt, proj.xMesh, proj.yMesh) + end + end +# +# Plot the model +# + if plotTheModel +# +# Plot the outer innerBoundaries +# + if !isempty(proj.outerBndryNames) + plotNumbers = ["O."*string(i) for i in 1:length(proj.outerBndryNames)] + plotNames = String[] + for j = 1:length(proj.outerBndryNames) + push!(plotNames, plotNumbers[j]*"| Outer."*proj.outerBndryNames[j] ) + end + plotChain!(plt,proj.outerBndryPoints, plotNames, plotNumbers) + end +# +# Plot the inner innerBoundaries +# + if !isempty(proj.innerBoundaryChainNames) + for i = 1:length(proj.innerBoundaryChainNames) + innerBndryPts = proj.innerBoundaryPoints[i] + innerBndryNames = [ proj.innerBoundaryChainNames[i]*"."*s for s in proj.innerBoundaryNames[i]] + plotNumbers = [string(i)*"."*string(j) for j in 1:length(innerBndryNames)] + plotNames = String[] + for j = 1:length(innerBndryNames) + push!(plotNames, plotNumbers[j]*"| "*innerBndryNames[j] ) + end + plotChain!(plt,innerBndryPts, plotNames, plotNumbers) + end + end + if !isempty(proj.outerBndryNames) || !isempty(proj.innerBoundaryChainNames) + plt[1,2] = Legend(plt, ax, "Curves", framevisible = false, labelsize = 24, titlesize = 28) + end + + end +# +# Plot refinement regions +# + if plotTheRefinements + if !isempty(proj.refinementRegionNames) + plotRefinement(plt,proj.refinementRegionPoints, + proj.refinementRegionNames, + proj.refinementRegionLoc) + end + end +# +# Display the plot +# + ax.aspect = DataAspect() + display(plt) +end + + +""" + updatePlot!(proj::Project) + +This version replots the figure with the current options. Legacy. +""" +function updatePlot!(proj::Project) + if !isnothing(proj.plt) + proj.plt = Figure(resolution = (1000, 1000)) + plotOptions = proj.plotOptions + plotProject!(proj, plotOptions) + end +end + + +""" + updatePlot!(proj::Project, plotOptions::Int) + +Replot with the new plotOptions = combinations (sums) of + + GRID, MESH, MODEL, REFINEMENTS + +Example: updatePlot!(p, MESH + MODEL) +""" +function updatePlot!(proj::Project, plotOptions::Int) + if !isnothing(proj.plt) + proj.plt = Figure(resolution = (1000, 1000)) + plotProject!(proj, plotOptions) + end +end + + +function plotChain!(plt, chainPoints::Array{Any}, legendLabels::Array{String}, curveLabels::Array{String} ) + x = chainPoints[1] + plotCurve(plt, x, legendLabels[1], curveLabels[1]) + + s = length(legendLabels) + + for i = 2:s + x = chainPoints[i] + plotCurve(plt,x,legendLabels[i], curveLabels[i]) + end +end + + +function plotCurve(plt, points::Matrix{Float64}, legendLabel::String, curveLabel::String) + lines!(plt[1,1],points[:,1],points[:,2], label = legendLabel, linewidth = 5 ) + s = size(points) + np = div(s[1], 2, RoundNearest) + if s[1] == 3 + np = 2 + end + # dx = points[np+1,1] - points[np-1,1] + # dy = points[np+1,2] - points[np-1,2] + # theta = atan(dy,dx) + # if(abs(dy) <= 0.0001) #Not pretty + # theta = 0.0 + # end + pp = (points[np,1],points[np,2]) + text!(plt[1,1],curveLabel,textsize = 28, position = pp, align = (:center,:center) ) +end + + +function plotRefinement(plt, points::Array{Matrix{Float64}}, label::Array{String}, loc::Array{Array{Float64}}) + + for (i,reg) in enumerate(points) + lines!(plt[1,1],reg[:,1],reg[:,2], label = label[i], linewidth = 5, linestyle = :dot, color=:black ) + p = loc[i] + pp = (p[1],p[2]) + text!(plt[1,1],label[i],position = pp, align = (:center,:center)) + end +end diff --git a/test/Project.toml b/test/Project.toml index bb8371ee..29ae53d8 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,3 +1,8 @@ [deps] +AbaqusReader = "bc6b9049-e460-56d6-94b4-a597b2c0390d" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -AbaqusReader = "bc6b9049-e460-56d6-94b4-a597b2c0390d" \ No newline at end of file + +[compat] +AbaqusReader = "0.2.5" +CairoMakie = "0.6, 0.7, 0.8" \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index c831d54b..70ff58a8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -22,7 +22,7 @@ isdir(outdir) && rm(outdir, recursive=true) @test generate_mesh(control_file, verbose=true) isa String end - @testset "generate_mesh() with ABAQUS output" begin + @testset "generate_mesh() in 2D with ABAQUS output" begin control_file = joinpath(HOHQMesh.examples_dir(), "IceCreamCone_Abaqus.control") generate_mesh(control_file) parse_mesh = abaqus_read_mesh(joinpath(outdir, "IceCreamCone_Abaqus.inp")) @@ -31,6 +31,15 @@ isdir(outdir) && rm(outdir, recursive=true) @test parse_mesh["elements"][114] == reference_ids end + @testset "generate_mesh() in 3D with ABAQUS output" begin + control_file = joinpath(HOHQMesh.examples_dir(), "HalfCircle3DRot.control") + generate_mesh(control_file) + parse_mesh = abaqus_read_mesh(joinpath(outdir, "HalfCircle3DRot.inp")) + # set some reference values for comparison. These are the corner IDs for element 246 + reference_ids = [297, 321, 322, 302, 363, 387, 388, 368] + @test parse_mesh["elements"][246] == reference_ids + end + @testset "generate_mesh() with invalid format" begin # Create temporary control file option that is invalid mktemp() do path, io @@ -42,5 +51,37 @@ isdir(outdir) && rm(outdir, recursive=true) end # testset "HOHQMesh.jl" +# Unit tests for the interactive mesh functionality + +# Background grid test routines +include("test_background_grid.jl") + +# Curve test routines +include("test_curve.jl") + +# Model test routines +include("test_model.jl") + +# Interactive mesh project test routines +include("test_interactive_project.jl") + +# Interactive mesh scripts available in examples folder +include("test_examples.jl") + +# Interactive mesh project + visualzation test routines +include("test_project_with_viz.jl") + +# Refinement test routines +include("test_refinement.jl") + +# Run parameters test routines +include("test_run_parameters.jl") + +# Smoother test routines +include("test_smoother.jl") + +# Visualization test routines +include("test_visualization.jl") + # Clean up afterwards: delete HOHQMesh output directory @test_nowarn rm(outdir, recursive=true) \ No newline at end of file diff --git a/test/test_background_grid.jl b/test/test_background_grid.jl new file mode 100644 index 00000000..16022821 --- /dev/null +++ b/test/test_background_grid.jl @@ -0,0 +1,83 @@ +module TestBackgroundGrid +#= + Background Grid Tests exercises routines found in "src/Project/BackgroundGridAPI.jl" functions + +Functions: @ = tested + @ addBackgroundGrid!(proj::Project, bgSize::Array{Float64}) + @ addBackgroundGrid!(proj::Project, box::Array{Float64}, N::Array{Int}) + @ addBackgroundGrid!(proj::Project, x0::Array{Float64}, dx::Array{Float64}, N::Array{Int}) + @ removeBackgroundGrid!(proj::Project) + @ setBackgroundGridSize!(proj::Project, dx::Float64, dy::Float64, dz::Float64 = 0.0) + @ getBackgroundGridSize(proj::Project) + @ getBackgroundGridLowerLeft(proj::Project) + @ getBackgroundGridSteps(proj::Project) + @ setBackgroundGridLowerLeft!(proj::Project, x0::Array{Float64}) + @ setBackgroundGridSteps!(proj::Project, N::Array{Int}) + @ setBackgroundGridSize!(proj::Project, dx::Array{Float64}, key::String) + @ addBackgroundGrid!(proj::Project, dict::Dict{String,Any}) +=# +using HOHQMesh +using Test + +@testset "Background Grid Tests" begin + + projectName = "TestProject" + projectPath = "out" + + p = newProject(projectName, projectPath) +# +# Add with method 1 (when outer boundary is present): [dx,dy,dz] +# + @test HOHQMesh.hasBackgroundGrid(p) == false + addBackgroundGrid!(p,[0.1,0.2,0.0]) + @test HOHQMesh.hasBackgroundGrid(p) == true + bgs = getBackgroundGridSize(p) + @test isapprox(bgs,[0.1,0.2,0.0]) + removeBackgroundGrid!(p) + @test HOHQMesh.hasBackgroundGrid(p) == false +# +# Add with method 2: lower left, dx, nPts +# + addBackgroundGrid!(p,[-1.0,-1.0,0.0],[0.1,0.1,0.0], [10,10,0]) + @test HOHQMesh.hasBackgroundGrid(p) == true + @test getBackgroundGridSteps(p) == [10,10,0] + @test isapprox(getBackgroundGridSize(p),[0.1,0.1,0.0]) + @test isapprox(getBackgroundGridLowerLeft(p),[-1.0,-1.0,0.0]) +# +# Test undo, redo +# + undo() + @test HOHQMesh.hasBackgroundGrid(p) == false + redo() + @test HOHQMesh.hasBackgroundGrid(p) == true + removeBackgroundGrid!(p) + @test HOHQMesh.hasBackgroundGrid(p) == false +# +# Add with method 3 (No outer bounday, preferred): bounding box + nPts +# + addBackgroundGrid!(p, [10.0,-10.0,-5.0,5.0], [10,10,0]) + @test HOHQMesh.hasBackgroundGrid(p) == true + @test getBackgroundGridSteps(p) == [10,10,0] + @test isapprox(getBackgroundGridSize(p),[1.5,1.5,0.0]) + @test isapprox(getBackgroundGridLowerLeft(p),[-10.0,-5.0,0.0]) +# +# Editing functions +# + setBackgroundGridSize!(p, 1.0, 1.0) + @test isapprox(getBackgroundGridSize(p), [1.0,1.0,0.0]) + @test getBackgroundGridSteps(p) == [15,15,0] + + removeBackgroundGrid!(p) + @test HOHQMesh.hasBackgroundGrid(p) == false +# +# There are no longer any background grid. Delete the notification center piece as well. +# Note that the notification center is global and can have multiple observers. So we test +# this notification center removal before other observers, e.g. other projects in the +# testing runs, are created that will add in the background grid again. +# + HOHQMesh.unRegisterForNotification(p, "BGRID_DID_CHANGE_NOTIFICATION") + @test haskey( HOHQMesh.HQMNotificationCenter , "BGRID_DID_CHANGE_NOTIFICATION" ) == false + +end + +end # module \ No newline at end of file diff --git a/test/test_curve.jl b/test/test_curve.jl new file mode 100644 index 00000000..bd548f34 --- /dev/null +++ b/test/test_curve.jl @@ -0,0 +1,240 @@ +module TestCurve +#= + Curve Tests tests the "CurvesAPI.jl" functions + +Functions: @ = tested + @(as new) newParametricEquationCurve(name::String, + xEqn::String, + yEqn::String, + zEqn::String = "z(t) = 0.0" ) + @(as new) newEndPointsLineCurve(name::String, + xStart::Array{Float64}, + xEnd::Array{Float64}) + @(as new) newCircularArcCurve(name::String, + center::Array{Float64}, + radius::Float64, + startAngle::Float64, + endAngle::Float64, + units::String = "degrees") + @ newSplineCurve(name::String, nKnots::Int, data::Matrix{Float64}) + newSplineCurve(name::String, dataFile::String) + @ setCurveName!(crv::Dict{String,Any}, name::String) + @ getCurveName(crv::Dict{String,Any}) + @ getCurveType(crv::Dict{String,Any}) + @ setXEqn!(crv::Dict{String,Any}, eqn::String) + @ getXEqn(crv::Dict{String,Any}) + @ setYEqn!(crv::Dict{String,Any}, eqn::String) + @ getYEqn(crv::Dict{String,Any}) + @ setZEqn!(crv::Dict{String,Any}, eqn::String) + @ getZEqn(crv::Dict{String,Any}) + @ setStartPoint!(crv::Dict{String,Any}, point::Array{Float64}) + @ setStartPoint!(crv::Dict{String,Any}, pointAsString::String) + @ getStartPoint(crv::Dict{String,Any}) + @ setEndPoint!(crv::Dict{String,Any}, point::Array{Float64}) + @ setEndPoint!(crv::Dict{String,Any}, pointAsString::String) + @ getEndPoint(crv::Dict{String,Any}) + @ setArcUnits!(arc::Dict{String,Any}, units::String) + @ getArcUnits(arc::Dict{String,Any}) + @ setArcCenter!(arc::Dict{String,Any}, point::Array{Float64}) + @ setArcCenter!(arc::Dict{String,Any}, pointAsString::String) + @ getArcCenter(arc::Dict{String,Any}) + @ setArcStartAngle!(arc::Dict{String,Any}, angle::Float64) + @ getArcStartAngle(arc::Dict{String,Any}) + @ setArcEndAngle!(arc::Dict{String,Any}, angle::Float64) + @ getArcEndAngle(arc::Dict{String,Any}) + @ setArcRadius!(arc::Dict{String,Any}, radius::Float64) + @ getArcRadius(arc::Dict{String,Any}) + @ setSplineNKnots!(spline::Dict{String,Any}, nKnots::Int) + @ getSplineNKnots(spline::Dict{String,Any}) + @ setSplinePoints!(spline::Dict{String,Any},points::Matrix{Float64}) + @ getSplinePoints(spline::Dict{String,Any}) +=# +using HOHQMesh +using Test + +@testset "Curve Tests" begin + @testset "ParametricCurve Tests" begin + xEqn = "x(t) = t" + yEqn = "y(t) = 2.0*t" + zEqn = "z(t) = 0.0" + name = "TestParametricCurve" + + crv = new(name, xEqn, yEqn, zEqn) + + @test typeof(crv) == Dict{String,Any} + @test getCurveType(crv) == "PARAMETRIC_EQUATION_CURVE" + @test getCurveName(crv) == name + @test getXEqn(crv) == xEqn + @test getYEqn(crv) == yEqn + @test getZEqn(crv) == zEqn + + value = HOHQMesh.curvePoint(crv, 0.25) + @test value == [0.25, 0.5, 0.0] + + # tests to reset curve name and equation definitions + setCurveName!(crv, "WhatACurve") + setXEqn!(crv, "t^2") + setYEqn!(crv, "1.5*t") + setZEqn!(crv, "t^3") + @test getCurveName(crv) == "WhatACurve" + @test getXEqn(crv) == "t^2" + @test getYEqn(crv) == "1.5*t" + @test getZEqn(crv) == "t^3" + + # If the equal sign is forgotten an error is thrown + fEqn = "f(t) 1.5*t" + @test_throws ErrorException HOHQMesh.keyAndValueFromString(fEqn) + end + + @testset "EndPointLine Tests" begin + xStart = [0.0,0.0,0.0] + xEnd = [1.0,1.0,0.0] + name = "EndPointLineCurve" + + crv = new(name,xStart,xEnd) + + @test typeof(crv) == Dict{String,Any} + @test getCurveType(crv) == "END_POINTS_LINE" + @test getCurveName(crv) == name + @test getStartPoint(crv) == xStart + @test getEndPoint(crv) == xEnd + + pt = HOHQMesh.curvePoint(crv, 0.5) + @test isapprox(pt,[0.5,0.5,0.0]) + + pts = HOHQMesh.curvePoints(crv, 2) + @test isapprox(pts[1,:],[0.0,0.0]) + @test isapprox(pts[2,:],[0.5,0.5]) + @test isapprox(pts[3,:],[1.0,1.0]) + + setStartPoint!(crv,[2.0,3.0,0.0]) + @test getStartPoint(crv) == [2.0,3.0,0.0] + undo() + @test getStartPoint(crv) == xStart + redo() + @test getStartPoint(crv) == [2.0,3.0,0.0] + + setEndPoint!(crv,[2.0,3.0,0.0]) + @test getEndPoint(crv) == [2.0,3.0,0.0] + undo() + @test getEndPoint(crv) == xEnd + redo() + @test getEndPoint(crv) == [2.0,3.0,0.0] + end + + @testset "CircularArc Tests" begin + center = [0.0,0.0,0.0] + radius = 2.0 + startAngleD = 0.0 + endAngleD = 180.0 + name = "CircularArcCurve" + + crv = new(name, center, radius, startAngleD, endAngleD, "degrees") + + @test typeof(crv) == Dict{String,Any} + @test getCurveType(crv) == "CIRCULAR_ARC" + @test getCurveName(crv) == name + @test getArcCenter(crv) == center + @test getArcRadius(crv) == radius + @test getArcStartAngle(crv) == startAngleD + @test getArcUnits(crv) == "degrees" + + pt = HOHQMesh.curvePoint(crv, 0.5) + @test isapprox(pt,[0.0,2.0,0.0]) + + pts = HOHQMesh.curvePoints(crv,2) + @test isapprox(pts[1,:],[2.0,0.0]) + @test isapprox(pts[2,:],[0.0,2.0]) + @test isapprox(pts[3,:],[-2.0,0.0]) + + # Purposly trigger warning with invalid units + @test_logs (:warn, "Units must either be `degrees` or `radians`. Try setting `units` again.") setArcUnits!(crv,"Rankine") + + setArcUnits!(crv,"radians") + @test getArcUnits(crv) == "radians" + undo() + @test getArcUnits(crv) == "degrees" + redo() + @test getArcUnits(crv) == "radians" + + setArcCenter!(crv,[1.0,2.0,0.0]) + @test getArcCenter(crv) == [1.0,2.0,0.0] + undo() + @test getArcCenter(crv) == center + redo() + @test getArcCenter(crv) == [1.0,2.0,0.0] + + setArcStartAngle!(crv, 90.0) + @test getArcStartAngle(crv) == 90.0 + setArcEndAngle!(crv, 270.0) + @test getArcEndAngle(crv) == 270.0 + setArcRadius!(crv, 1.5) + @test getArcRadius(crv) == 1.5 + end + + @testset "Spline Tests" begin + nKnots = 5 + nPts = 3 + data = zeros(Float64,nKnots,4) + for j in 1:5 + tj = 0.25*(j-1) + xj = tj^3 + yj = tj^3 + tj^2 + zj = 0.0 + data[j,:] = [tj,xj,yj,zj] + end + + name = "Spline Curve" + crv = newSplineCurve(name, nKnots, data) + + @test typeof(crv) == Dict{String,Any} + @test getCurveType(crv) == "SPLINE_CURVE" + @test getCurveName(crv) == name + @test getSplineNKnots(crv) == nKnots + + pt = HOHQMesh.curvePoint(crv, 0.5) + @test isapprox(pt, [0.5^3, 0.5^3 + 0.5^2, 0.0]) + pt = HOHQMesh.curvePoint(crv, 0.0) + @test isapprox(pt, [0.0, 0.0, 0.0]) +# +# The curvePoints for the spline has M = max(N,nKnots*2) values +# + M = max(nPts, nKnots*2) + pts = HOHQMesh.curvePoints(crv, M) + d = 1.0 / M + for j in 1:M+1 + tj = (j-1) * d + @test isapprox(pts[j, :], [tj^3, tj^3 + tj^2]) + end + + gPts = getSplinePoints(crv) + @test isapprox(data, gPts) +# +# Create a new set of points to replace the existing ones +# + data = zeros(Float64, 10, 4) + for j in 1:10 + tj = 0.1*(j-1) + xj = tj^4 + yj = tj^2 + 0.5*tj + zj = 0.0 + data[j,:] = [tj,xj,yj,zj] + end + setSplineNKnots!(crv, 10) + setSplinePoints!(crv, data) + # test the new points + gPts = getSplinePoints(crv) + @test isapprox(data, gPts) +# +# Get spline data from a file +# + fSpline = newSplineCurve("fromFile", joinpath(@__DIR__, "test_spline_curve_data.txt")) + fPts = getSplinePoints(fSpline) + # Use point value 11 from the file as the test point + control_pt = [0.131578947368421, -0.657970237227418, 0.051342865473934, 0.0] + @test isapprox(control_pt, fPts[11, :]) + end + +end + +end # module \ No newline at end of file diff --git a/test/test_examples.jl b/test/test_examples.jl new file mode 100644 index 00000000..725d3e3f --- /dev/null +++ b/test/test_examples.jl @@ -0,0 +1,40 @@ +module TestInteractiveExamples + +using HOHQMesh +using Test + +# We use CairoMakie to avoid some CI-related issues with GLMakie. CairoMakie can be used +# as a testing backend for interactive mesh tool's Makie-based visualization. +#using CairoMakie + +@testset "Interactive Examples Tests" begin + + test_file = joinpath(HOHQMesh.examples_dir(), "interactive_from_control_file.jl") + include(test_file) + @test p.name == "AllFeatures" + + test_file = joinpath(HOHQMesh.examples_dir(), "interactive_outer_boundary.jl") + include(test_file) + @test p.name == "IceCreamCone" + + test_file = joinpath(HOHQMesh.examples_dir(), "interactive_outer_boundary_generic.jl") + include(test_file) + N = getPolynomialOrder(p) + # Test against the default polynomial order of 5 that gets reset in this include + @test N == 5 + + test_file = joinpath(HOHQMesh.examples_dir(), "interactive_outer_box_two_circles.jl") + include(test_file) + format = getMeshFileFormat(p) + # Test against the mesh file format type for this example + @test format == "ABAQUS" + + test_file = joinpath(HOHQMesh.examples_dir(), "interactive_spline_curves.jl") + include(test_file) + spline = getCurve(p, "big_spline", "inner1") + # Test against the number of spline knots from the testing file + @test spline["nKnots"] == "77" + +end + +end #module \ No newline at end of file diff --git a/test/test_interactive_project.jl b/test/test_interactive_project.jl new file mode 100644 index 00000000..caa1c4cf --- /dev/null +++ b/test/test_interactive_project.jl @@ -0,0 +1,92 @@ +module TestInteractiveMeshProject +#= + Project Tests tests the "Project.jl" functions + +Functions: @ = tested + + @ openProject(fileName::String, folder::String) + @ saveProject(proj::Project) + @ newProject(name::String, folder::String) + @ hasBackgroundGrid(proj::Project) + @@ assemblePlotArrays(proj::Project) + @ projectBounds(proj::Project) + @ projectGrid(proj::Project) + + curveDidChange(proj::Project,crv::Dict{String,Any}) + modelDidChange(proj::Project, sender::Project) + backgroundGridDidChange(proj::Project, sender::Project) + refinementWasAdded(proj::Project, sender::Project) + refinementDidChange(proj::Project, sender::Dict{String,Any}) + meshWasGenerated(proj::Project, sender::Project) + meshWasDeleted(proj::Project, sender::Project) + +=# +using HOHQMesh +using Test + +@testset "Project Tests" begin +# +# Create, save, and read +# + projectName = "TestProject" + projectPath = "out" + + p = newProject(projectName, projectPath) + + saveProject(p) + q = openProject("TestProject.control", projectPath) + setSmoothingIterations!(q,25) + + @test getSmoothingIterations(q) == 25 + @test getSmoothingStatus(q) == "ON" + @test getSmoothingType(q) == "LinearAndCrossbarSpring" + + cDict = HOHQMesh.getControlDict(q) + @test haskey(cDict,"SPRING_SMOOTHER") == true + removeSpringSmoother!(q) + cDict = HOHQMesh.getControlDict(q) + @test haskey(cDict,"SPRING_SMOOTHER") == false + + # read in the AllFeatures example + control_file = joinpath(HOHQMesh.examples_dir(), "AllFeatures.control") + p = openProject(control_file, projectPath) + + @test HOHQMesh.hasBackgroundGrid(p) == true + bounds = [25.28, -20.0, -5.0, 20.0] + @test isapprox(p.bounds,bounds) + refinementNames = ["center", "line"] + @test p.refinementRegionNames == refinementNames + @test isapprox(p.refinementRegionLoc[1],[9.0,-3.0]) + + obNames = ["B1", "B2", "B3"] + @test p.outerBndryNames == obNames + + xGrid = [-23.0, -20.0, -17.0, -14.0, -11.0, -8.0, -5.0, -2.0, 1.0, 4.0, 7.0, + 10.0, 13.0, 16.0, 19.0, 22.0] + yGrid = [-8.0, -5.0, -2.0, 1.0, 4.0, 7.0, 10.0, 13.0, 16.0, 19.0, 22.0, 25.0, 28.0] + p.xGrid, p.yGrid = HOHQMesh.projectGrid(p) + @test isapprox(p.xGrid,xGrid) + @test isapprox(p.yGrid,yGrid) + + # Exercise some dictionary printing routines for the AllFeatures project + HOHQMesh.showDescription(p.projectDictionary) + HOHQMesh.stringForKeyFromDictionary("CONTROL_INPUT", p.projectDictionary) + + # Use the NACA0012 example because it sets the background grid differently + control_file = joinpath(HOHQMesh.examples_dir(), "NACA0012.control") + p = openProject(control_file, projectPath) + + sizes = [2.0, 2.0, 1.0] + @test isapprox( getBackgroundGridSize(p) , sizes ) + steps = [20, 20 ,20] + @test isapprox( getBackgroundGridSteps(p), steps ) + lower_left = [-20.0 , -20.0 , 0.0] + @test isapprox( getBackgroundGridLowerLeft(p) , lower_left ) + # Update the background grid starting point + new_lower_left = [-25.0, -10.0, 0.0] + HOHQMesh.setBackgroundGridLowerLeft!(p, new_lower_left) + @test isapprox( getBackgroundGridLowerLeft(p) , new_lower_left ) + +end + +end # module \ No newline at end of file diff --git a/test/test_model.jl b/test/test_model.jl new file mode 100644 index 00000000..7fa92f71 --- /dev/null +++ b/test/test_model.jl @@ -0,0 +1,145 @@ +module TestModel +#= + Model Tests tests the "ModelAPI.jl" functions + +Functions: @ = tested + @@ = indirectly tested through other tests + + @ addCurveToOuterBoundary!(proj::Project, crv::Dict{String,Any}) + @@ removeOuterBoundaryCurveWithName!(proj::Project, name::String) + @ getOuterBoundaryCurveWithName(proj::Project, name::String) + @@ insertOuterBoundaryCurveAtIndex!(proj::Project, crv::Dict{String,Any}, indx::Int) + @@ removeOuterBoundaryCurveAtIndex!(proj::Project, indx::Int) + @@ addOuterBoundary!(proj::Project, outerBoundary::Dict{String,Any}) + @ removeOuterBoundary!(proj::Project) + @ getOuterBoundaryChainList(proj::Project) + + @@ addCurveToInnerBoundary!(proj::Project, crv::Dict{String,Any}, boundaryName::String) + @ removeInnerBoundaryCurve!(proj::Project, name::String, chainName::String) + @@ insertInnerBoundaryCurveAtIndex!(proj::Project, crv::Dict{String,Any}, + indx::Int, boundaryName::String) + @@ removeInnerBoundaryCurveAtIndex!(proj::Project, indx::Int, chainName::String) + removeInnerBoundary!(proj::Project, chainName::String) + @@ addInnerBoundaryWithName!(proj::Project,name::String) + @ getChainIndex(chain::Vector{Dict{String, Any}},name) + @@ getAllInnerBoundaries(proj::Project) + @ getInnerBoundaryChainWithName(proj::Project, name::String) + @ getInnerBoundaryCurve(proj::Project, curveName::String, boundaryName::String) + innerBoundaryIndices(proj::Project, curveName::String) + + @ getModelDict(proj::Project) + @@ getDictInModelDictNamed(proj::Project,name::String) +=# +using HOHQMesh +using Test + +@testset "Model Tests" begin +# +# Exercise the different outputs for empty undo / redo stacks +# + clearUndoRedo() + @test undo() == "Empty undo stack. No action performed." + @test undoActionName() == "No undo action in queue" + @test redo() == "Empty redo stack. No action performed." + @test redoActionName() == "No redo action in queue" +# +# Project for the model +# + projectName = "TestProject" + projectPath = "out" + + p = newProject(projectName, projectPath) +# +# Create some boundary curves +# + obc1 = new("obc1",[0.0,0.0,0.0], [2.0,0.0,0.0]) + obc2 = new("obc2",[2.0,0.0,0.0], [1.0,1.0,0.0]) + obc3 = new("obc3",[1.0,1.0,0.0], [0.0,0.0,0.0]) +# + add!(p,obc1) + add!(p,obc2) + addCurveToOuterBoundary!(p,obc3) + + obList = getOuterBoundaryChainList(p) + @test length(obList) == 3 + @test getChainIndex(obList,"obc3") == 3 + @test undoActionName() == "Add Outer Boundary Curve" + undo() + + @test length(obList) == 2 + + # Check the outer boundary curve that are not conneted. Throws a warning + @test_logs (:warn, "The boundary curve Outer is not closed. Fix to generate mesh" ) HOHQMesh.modelCurvesAreOK(p) + @test HOHQMesh.modelCurvesAreOK(p) == false + + @test redoActionName() == "Remove Outer Boundary Curve" + redo() + @test length(obList) == 3 + + # Outer boundary is connected again. Check is successful now + @test HOHQMesh.modelCurvesAreOK(p) == true + + crv = getOuterBoundaryCurveWithName(p,"obc2") + @test getCurveName(crv) == "obc2" +# +# Test remove/add outer boundary +# + + # Attempt to remove an outer boundary curve that does not exist. Throws an error + @test_throws ErrorException removeOuterBoundaryCurveWithName!(p, "wrongName") + + removeOuterBoundary!(p) + mDict = HOHQMesh.getModelDict(p) + @test haskey(mDict,"OUTER_BOUNDARY") == false + undo() + @test haskey(mDict,"OUTER_BOUNDARY") == true + crv = getOuterBoundaryCurveWithName(p,"obc2") + @test getCurveName(crv) == "obc2" + redo() + @test haskey(mDict,"OUTER_BOUNDARY") == false +# +# Inner boundary curve tests +# + ib1Name = "Inner1" + add!(p,obc1,ib1Name) + add!(p,obc2,ib1Name) + add!(p,obc3,ib1Name) + + i, chain = getInnerBoundaryChainWithName(p,ib1Name) + ibList = chain["LIST"] + @test length(ibList) == 3 + + ibc = getInnerBoundaryCurve(p, "obc2",ib1Name) + @test getCurveName(ibc) == "obc2" + + removeInnerBoundaryCurve!(p,"obc2",ib1Name) + @test length(ibList) == 2 + # Check the inner boundary curve that are not conneted. Throws a warning + @test_logs (:warn, "The curve obc3 does not meet the previous curve, obc1.") HOHQMesh.modelCurvesAreOK(p) + @test HOHQMesh.modelCurvesAreOK(p) == false + + undo() + @test length(ibList) == 3 + ibc = getInnerBoundaryCurve(p, "obc2",ib1Name) + @test getCurveName(ibc) == "obc2" + +# +# Purposely create outer / inner boundary curves that do not join in a new project. +# Attempt to generate a mesh and trigger an appropriate warning statement. +# + obc1 = new("obc1",[0.0,0.0,0.0], [2.0,0.0,0.0]) + obc2 = new("obc2",[3.0,0.0,0.0], [1.0,1.0,0.0]) + + # A background grid is required for the mesh generation call + addBackgroundGrid!(p, [0.5, 0.5, 0.0]) + + # Failing outer boundary + add!(p, obc1) + add!(p, obc2) + + # This call actually throws multiple warnings but we just test that the main one is thrown + @test_logs (:warn, "Meshing aborted: Ensure boundary curve segments are in order and boundary curves are closed and try again.") match_mode=:any generate_mesh(p) + +end + +end # module \ No newline at end of file diff --git a/test/test_project_with_viz.jl b/test/test_project_with_viz.jl new file mode 100644 index 00000000..6b77cf52 --- /dev/null +++ b/test/test_project_with_viz.jl @@ -0,0 +1,91 @@ +module TestProjectAndVisualization + +using HOHQMesh +using Test + +# We use CairoMakie to avoid some CI-related issues with GLMakie. CairoMakie can be used +# as a testing backend for interactive mesh tool's Makie-based visualization. +using CairoMakie + +@testset "Project with Visualization Tests" begin + @testset "Project From Scratch" begin + + projectName = "fromScratch" + projectPath = "out" + + p_scratch = newProject(projectName, projectPath) + + # Bounding box uses order [TOP, LEFT, BOTTOM, RIGHT] + bounds = [9.0, -8.0, -8.0, 8.0] + N = [16, 17, 1] + + # Lay the background grid and plot it + addBackgroundGrid!(p_scratch, bounds, N) + plotProject!(p_scratch, GRID) + + # Build the outer boundary chain and plot piece-by-piece + outer_line1 = newEndPointsLineCurve("outerline1", [0.0, -7.0, 0.0], [4.0, 3.0, 0.0]) + add!(p_scratch, outer_line1) + # Update the endpoint to trigger update plot from `curveDidChange` + setEndPoint!(outer_line1, [5.0, 3.0, 0.0]) + outer_arc = newCircularArcCurve("outerarc", [0.0, 3.0, 0.0], 5.0, 0.0, 180.0, "degrees") + add!(p_scratch, outer_arc) + outer_line2 = newEndPointsLineCurve("outerline2", [-5.0, 3.0, 0.0], [0.0, -7.0, 0.0]) + add!(p_scratch, outer_line2) + + # Check the computed background grid against expected values + x_grid_control = [-8.0, -7.0, -6.0, -5.0, -4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0] + y_grid_control = [-8.0, -7.0, -6.0, -5.0, -4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] + p_scratch.xGrid, p_scratch.yGrid = HOHQMesh.projectGrid(p_scratch) + + @test isapprox(p_scratch.xGrid, x_grid_control) + @test isapprox(p_scratch.yGrid, y_grid_control) + + # Build the inner pill-shaped boundary chain and plot it piece-by-piece + inner_line1 = newEndPointsLineCurve("innerLine1", [-1.0, 5.0, 0.0], [1.0, 3.0, 0.0]) + add!(p_scratch, inner_line1, "inner1") + setStartPoint!(inner_line1, [1.0, 5.0, 0.0]) + inner_bottom_arc = newCircularArcCurve("innerBottomArc", [0.0, 3.0, 0.0], 1.0, 0.0, -180.0, "degrees") + add!(p_scratch, inner_bottom_arc, "inner1") + inner_line2 = newEndPointsLineCurve("innerLine2", [-1.0, 3.0, 0.0], [-1.0, 5.0, 0.0]) + add!(p_scratch, inner_line2, "inner1") + inner_top_arc = newCircularArcCurve("innerTopArc", [0.0, 5.0, 0.0], 1.0, 180.0, 0.0, "degrees") + add!(p_scratch, inner_top_arc, "inner1") + + # Add in a refinement center and adjust its width manually + cent = newRefinementCenter("center1", "smooth", [0.0, -1.0, 0.0], 0.25, 1.0) + add!(p_scratch, cent) + setRefinementWidth!(cent, 0.5) + + # Generate the mesh (automatically updates the plot) + @test_nowarn generate_mesh(p_scratch) + + end + + @testset "Project From File" begin + + projectPath = "out" + + control_file = joinpath(HOHQMesh.examples_dir(), "AllFeatures.control") + p_file = openProject(control_file, projectPath) + + @test_nowarn plotProject!(p_file, MODEL+GRID) + + # Refinement regions are already present but we manually adjust + # one of its parameters to trigger a specific piece of plot update logic + # in `refinementDidChange` + _, refine_center = getRefinementRegion(p_file, "center") + setRefinementGridSize!(refine_center, 0.15) + @test getRefinementGridSize(refine_center) == 0.15 + + # Adjust the background grid size + setBackgroundGridSize!(p_file, 4.0, 4.0, 0.0) + @test getBackgroundGridSize(p_file) == [4.0, 4.0, 0.0] + + # Generate the mesh (automatically updates the plot) + @test_nowarn generate_mesh(p_file) + + end +end + +end #module \ No newline at end of file diff --git a/test/test_refinement.jl b/test/test_refinement.jl new file mode 100644 index 00000000..8954a686 --- /dev/null +++ b/test/test_refinement.jl @@ -0,0 +1,175 @@ +module TestRefinement +#= + Project Tests tests the "RefinementRegions.jl" functions +Functions: @ = tested + @ newRefinementCenter + @ addRefinementRegion! + addRefinementRegionPoints! + refinementRegionPoints + @ getRefinementRegionCenter + @ removeRefinementRegion! + @ insertRefinementRegion! + @ newRefinementLine + @ getRefinementRegion (1) + @ getAllRefinementRegions + @ getRefinementRegion (2) + @ setRefinementType! + @ getRefinementType + @ setRefinementName! + @ getRefinementName + @ setRefinementLocation! + @ getRefinementLocation + @ setRefinementGridSize! + @ getRefinementGridSize + @ setRefinementWidth! + @ getRefinementWidth + @ setRefinementStart! + @ getRefinementStart + @ setRefinementEnd! + @ getRefinementEnd +=# +using HOHQMesh +using Test + +@testset "Refinement Tests" begin + + projectName = "TestProject" + projectPath = "out" + + p = newProject(projectName, projectPath) +# +# Creating and changing refinement regions... +# + x0 = [1.0,2.0,0.0] + h = 0.25 + w = 0.5 +# +# ...Center +# + cent1 = newRefinementCenter("Center1","smooth",x0,h,w) + addRefinementRegion!(p,cent1) + @test length(p.refinementRegionNames) == 1 +# +# Refinement type +# + @test getRefinementType(cent1) == "smooth" + setRefinementType!(cent1,"sharp") + @test getRefinementType(cent1) == "sharp" + undo() + @test getRefinementType(cent1) == "smooth" + redo() + @test getRefinementType(cent1) == "sharp" +# +# Refinement name +# + @test getRefinementName(cent1) == "Center1" + setRefinementName!(cent1,"Second") + @test getRefinementName(cent1) == "Second" + undo() + @test getRefinementName(cent1) == "Center1" + redo() + @test getRefinementName(cent1) == "Second" + undo() + @test getRefinementName(cent1) == "Center1" +# +# Refinement center location +# + @test getRefinementLocation(cent1) == x0 + @test getRefinementRegionCenter(cent1) == [1.0,2.0] + setRefinementLocation!(cent1,[0.,0.,0.]) + @test getRefinementLocation(cent1) == [0.,0.,0.] + undo() + @test getRefinementLocation(cent1) == x0 + redo() + @test getRefinementLocation(cent1) == [0.,0.,0.] +# +# Refinement width +# + @test getRefinementWidth(cent1) == w + setRefinementWidth!(cent1,1.0) + @test getRefinementWidth(cent1) == 1.0 + undo() + @test getRefinementWidth(cent1) == w + redo() + @test getRefinementWidth(cent1) == 1.0 +# +# Refinement grid size +# + @test getRefinementGridSize(cent1) == h + setRefinementGridSize!(cent1,0.5) + @test getRefinementGridSize(cent1) == 0.5 + undo() + @test getRefinementGridSize(cent1) == h + redo() + @test getRefinementGridSize(cent1) == 0.5 +# +#... Line +# + line1 = newRefinementLine("Line1","smooth",[1.0,0.5,0.0],[1.5,2.0,0.0],h,w) + addRefinementRegion!(p,line1) + @test length(p.refinementRegionNames) == 2 + #the following have been tested above + @test getRefinementType(line1) == "smooth" + @test getRefinementName(line1) == "Line1" + @test getRefinementWidth(line1) == w + @test getRefinementGridSize(line1) == h +# +# Refinement line start +# + @test isapprox(getRefinementStart(line1),[1.0,0.5,0.0]) + setRefinementStart!(line1,[0.0,0.0,0.0]) + @test getRefinementStart(line1) == [0.0,0.0,0.0] + undo() + @test isapprox(getRefinementStart(line1),[1.0,0.5,0.0]) + redo() + @test getRefinementStart(line1) == [0.0,0.0,0.0] +# +# Refinement Line End +# + @test isapprox(getRefinementEnd(line1),[1.5,2.0,0.0]) + setRefinementEnd!(line1,[0.0,0.0,0.0]) + @test getRefinementEnd(line1) == [0.0,0.0,0.0] + undo() + @test isapprox(getRefinementEnd(line1),[1.5,2.0,0.0]) + redo() + @test getRefinementEnd(line1) == [0.0,0.0,0.0] +# +# Project functions +# + lst = getAllRefinementRegions(p) + @test length(lst) == 2 + + (i,r) = getRefinementRegion(p,"Line1") + @test i == 2 + @test getRefinementName(r) == "Line1" + + s = getRefinementRegion(p,1) + @test getRefinementName(s) == "Center1" + + # Query for a refinement region that does not exist. Throws an error. + @test_throws ErrorException (i,r) = getRefinementRegion(p, "Line100") + + # Test that an error is thrown if one requests a refinement region with + # with an index larger than the number of regions present in the project + @test_throws ErrorException getRefinementRegion(p, 3) + + c2 = newRefinementCenter("middle","smooth",[2.0,3.0,4.0],0.6,3.0) + # Attempt to set an refinement type. Simply throws a warning to "Try again" + @test_logs (:warn, "Acceptable refinement types are `smooth` and `sharp`. Try again.") setRefinementType!(c2, "fancy") + + insertRefinementRegion!(p,c2,2) + lst = getAllRefinementRegions(p) + @test length(lst) == 3 + + removeRefinementRegion!(p,"middle") + lst = getAllRefinementRegions(p) + @test length(lst) == 2 + names = ["Center1", "Line1"] + + for (i,d) in enumerate(lst) + @test getRefinementName(d) == names[i] + end + +end + +end # module \ No newline at end of file diff --git a/test/test_run_parameters.jl b/test/test_run_parameters.jl new file mode 100644 index 00000000..8064bc69 --- /dev/null +++ b/test/test_run_parameters.jl @@ -0,0 +1,91 @@ +module TestRunParameters +#= + Run Parameters Tests tests the "RunParameters.jl" functions + +Functions: @ = tested + @ addRunParameters!(proj::Project, + plotFormat::String = "skeleton", + meshFileFormat::String = "ISM-V2", + polynomialOrder::Int = 5) + @ removeRunParameters!(proj::Project) + @ setName!(proj::Project,name::String) + @ getName(proj::Project) + @ setPolynomialOrder!(proj::Project, p::Int) + @ getPolynomialOrder(proj::Project) + @ setMeshFileFormat!(proj::Project, meshFileFormat::String) + @ getMeshFileFormat(proj::Project) + @ setPlotFileFormat!(proj::Project, plotFileFormat::String) + @ getPlotFileFormat(proj::Project) + @ setFileNames!(proj::Project) + @ getMeshFileName(proj::Project) + @ getPlotFileName(proj::Project) + @ getStatsFileName(proj::Project) +=# +using HOHQMesh +using Test + +@testset "Run Parameters Tests" begin + + projectName = "TestProject" + projectPath = "out" + newName = "RPTestsName" + + p = newProject(projectName, projectPath) # Auto sets up run parameters + + @test getName(p) == projectName + setName!(p,newName) + @test getName(p) == newName + + undo() + @test getName(p) == projectName + redo() + @test getName(p) == newName + + setFileNames!(p, getMeshFileFormat(p)) + # Use string concatenation to make this more general + @test getMeshFileName(p) == joinpath(projectPath, newName*".mesh") + @test getPlotFileName(p) == joinpath(projectPath, newName*".tec") + @test getStatsFileName(p) == joinpath(projectPath, newName*".txt") + + @test getPolynomialOrder(p) == 5 + setPolynomialOrder!(p,6) + @test getPolynomialOrder(p) == 6 + undo() + @test getPolynomialOrder(p) == 5 + redo() + @test getPolynomialOrder(p) == 6 + + setMeshFileFormat!(p, "ABAQUS") + @test getMeshFileFormat(p) == "ABAQUS" + undo() + + # ISM-V2 is the default file format type + @test getMeshFileFormat(p) == "ISM-V2" + setMeshFileFormat!(p,"ISM") + @test getMeshFileFormat(p) == "ISM" + undo() + @test getMeshFileFormat(p) == "ISM-V2" + redo() + @test getMeshFileFormat(p) == "ISM" + + @test_logs (:warn, "Acceptable file formats are: `ISM-V2`, `ISM`, or `ABAQUS`. Try again.") setMeshFileFormat!(p,"BLORP") + @test getMeshFileFormat(p) == "ISM" + + @test getPlotFileFormat(p) == "skeleton" + setPlotFileFormat!(p,"sem") + @test getPlotFileFormat(p) == "sem" + undo() + @test getPlotFileFormat(p) == "skeleton" + redo() + @test getPlotFileFormat(p) == "sem" + + @test_logs (:warn, "Acceptable plot formats are: `sem` or `skeleton`. Try again.") setPlotFileFormat!(p,"BLORP") + @test getPlotFileFormat(p) == "sem" + + removeRunParameters!(p) + cDict = HOHQMesh.getControlDict(p) + @test haskey(cDict,"RUN_PARAMETERS") == false + +end + +end # module \ No newline at end of file diff --git a/test/test_smoother.jl b/test/test_smoother.jl new file mode 100644 index 00000000..fd0eb522 --- /dev/null +++ b/test/test_smoother.jl @@ -0,0 +1,62 @@ +module TestSmoother +#= + Smoother Tests tests the "SmootherAPI.jl" functions + +Functions: @ = tested + @ addSpringSmoother!(status::String, type::String, nIterations::Int) + @ setSmoothingStatus!(proj::Project, status::String) + @ getSmoothingStatus(proj::Project) + @ setSmoothingType!(proj::Project, type::String) + @ getSmoothingType(proj::Project) + @ setSmoothingIterations!(proj::Project, iterations::Int) + @ getSmoothingIterations(proj::Project) + @ removeSpringSmoother!(proj::Project) +=# +using HOHQMesh +using Test + +@testset "Smoother Tests" begin +# +# Create, save, and read +# + projectName = "TestProject" + projectPath = "out" + + p = newProject(projectName, projectPath) + + saveProject(p) + q = openProject( projectName*".control", projectPath ) + + # Trigger error statements by setting incorrect values in the smoother options + @test_logs (:warn, "Acceptable smoother status are: `ON` or `OFF`. Try again.") addSpringSmoother!(p, "PAUSE", "LinearSpring", 50) + @test_logs (:warn, "Acceptable smoothers are: `LinearAndCrossbarSpring` or `LinearSpring`. Try again.") addSpringSmoother!(p, "ON" , "MagicSprings", 50) + + setSmoothingIterations!(q, 25) + + @test getSmoothingIterations(q) == 25 + @test getSmoothingStatus(q) == "ON" + @test getSmoothingType(q) == "LinearAndCrossbarSpring" + + setSmoothingStatus!(q,"OFF") + @test getSmoothingStatus(q) == "OFF" + + # Trigger error statement by setting an invalid spring status + @test_logs (:warn, "Acceptable smoother status is either: `ON` or `OFF`. Try again.") setSmoothingStatus!(q,"UNKNOWN") + @test getSmoothingStatus(q) == "OFF" + + setSmoothingType!(q,"LinearSpring") + @test getSmoothingType(q) == "LinearSpring" + + # Trigger error statement by setting an invalid spring type + @test_logs (:warn, "Acceptable smoothers are: `LinearAndCrossbarSpring` or `LinearSpring`. Try again.") setSmoothingType!(q,"TorsionalSpring") + @test getSmoothingType(q) == "LinearSpring" + + cDict = HOHQMesh.getControlDict(q) + @test haskey(cDict,"SPRING_SMOOTHER") == true + removeSpringSmoother!(q) + cDict = HOHQMesh.getControlDict(q) + @test haskey(cDict,"SPRING_SMOOTHER") == false + +end + +end # module \ No newline at end of file diff --git a/test/test_spline_curve_data.txt b/test/test_spline_curve_data.txt new file mode 100644 index 00000000..2e576e1d --- /dev/null +++ b/test/test_spline_curve_data.txt @@ -0,0 +1,78 @@ +77 +0.000000000000000 -1.000000000000000 -1.000000000000000 0.000000000000000 +0.013157894736842 -0.995929487847041 -0.984513308895507 0.000000000000000 +0.026315789473684 -0.984006935621602 -0.940263574448587 0.000000000000000 +0.039473684210526 -0.964665819673838 -0.870566304959074 0.000000000000000 +0.052631578947368 -0.938339616353904 -0.778737008726803 0.000000000000000 +0.065789473684211 -0.905461802011955 -0.668091194051611 0.000000000000000 +0.078947368421053 -0.866465852998146 -0.541944369233333 0.000000000000000 +0.092105263157895 -0.821785245662633 -0.403612042571803 0.000000000000000 +0.105263157894737 -0.771853456355570 -0.256409722366859 0.000000000000000 +0.118421052631579 -0.717103961427114 -0.103652916918335 0.000000000000000 +0.131578947368421 -0.657970237227418 0.051342865473934 0.000000000000000 +0.144736842105263 -0.594885760106638 0.205262116510112 0.000000000000000 +0.157894736842105 -0.528284006414929 0.354789327890363 0.000000000000000 +0.171052631578947 -0.458598452502447 0.496608991314852 0.000000000000000 +0.184210526315789 -0.386262574719347 0.627405598483744 0.000000000000000 +0.197368421052632 -0.311709849415783 0.743863641097203 0.000000000000000 +0.210526315789474 -0.235373752941912 0.842667610855393 0.000000000000000 +0.223684210526316 -0.157687761647887 0.920501999458480 0.000000000000000 +0.236842105263158 -0.079085351883865 0.974051298606627 0.000000000000000 +0.250000000000000 0.000000000000000 1.000000000000000 0.000000000000000 +0.263157894736842 0.079132214191990 0.996076583425322 0.000000000000000 +0.276315789473684 0.157864797034136 0.964185481015558 0.000000000000000 +0.289473684210526 0.235748651406911 0.907275112990232 0.000000000000000 +0.302631578947368 0.312334680190782 0.828293899568867 0.000000000000000 +0.315789473684211 0.387173786266220 0.730190260970987 0.000000000000000 +0.328947368421053 0.459816872513694 0.615912617416116 0.000000000000000 +0.342105263157895 0.529814841813676 0.488409389123779 0.000000000000000 +0.355263157894737 0.596718597046633 0.350628996313498 0.000000000000000 +0.368421052631579 0.660079041093037 0.205519859204799 0.000000000000000 +0.381578947368421 0.719447076833358 0.056030398017204 0.000000000000000 +0.394736842105263 0.774373607148064 -0.094890967029763 0.000000000000000 +0.407894736842105 0.824409534917626 -0.244295815716577 0.000000000000000 +0.421052631578947 0.869105763022515 -0.389235727823714 0.000000000000000 +0.434210526315789 0.908013194343199 -0.526762283131652 0.000000000000000 +0.447368421052632 0.940682731760148 -0.653927061420865 0.000000000000000 +0.460526315789474 0.966665278153833 -0.767781642471830 0.000000000000000 +0.473684210526316 0.985511736404724 -0.865377606065024 0.000000000000000 +0.486842105263158 0.996773009393289 -0.943766531980922 0.000000000000000 +0.500000000000000 1.000000000000000 -1.000000000000000 0.000000000000000 +0.513157894736842 0.994898517068294 -1.032013465103201 0.000000000000000 +0.526315789473684 0.981793993293483 -1.041277883073334 0.000000000000000 +0.539473684210526 0.961166767333847 -1.030148084893675 0.000000000000000 +0.552631578947368 0.933497177847667 -1.000978901547498 0.000000000000000 +0.565789473684211 0.899265563493221 -0.956125164018079 0.000000000000000 +0.578947368421053 0.858952262928790 -0.897941703288692 0.000000000000000 +0.592105263157895 0.813037614812655 -0.828783350342616 0.000000000000000 +0.605263157894737 0.762001957803095 -0.751004936163122 0.000000000000000 +0.618421052631579 0.706325630558390 -0.666961291733489 0.000000000000000 +0.631578947368421 0.646488971736821 -0.579007248036990 0.000000000000000 +0.644736842105263 0.582972319996667 -0.489497636056901 0.000000000000000 +0.657894736842105 0.516256013996209 -0.400787286776498 0.000000000000000 +0.671052631578947 0.446820392393727 -0.315231031179056 0.000000000000000 +0.684210526315789 0.375145793847500 -0.235183700247849 0.000000000000000 +0.697368421052632 0.301712557015808 -0.163000124966155 0.000000000000000 +0.710526315789474 0.227001020556933 -0.101035136317247 0.000000000000000 +0.723684210526316 0.151491523129152 -0.051643565284402 0.000000000000000 +0.736842105263158 0.075664403390749 -0.017180242850895 0.000000000000000 +0.750000000000000 0.000000000000000 0.000000000000000 0.000000000000000 +0.763157894736842 -0.075055193385125 -0.001715681169683 0.000000000000000 +0.776315789473684 -0.149190063107908 -0.020972184616666 0.000000000000000 +0.789473684210526 -0.222127340511945 -0.055672422052361 0.000000000000000 +0.802631578947368 -0.293589756940828 -0.103719305188178 0.000000000000000 +0.815789473684211 -0.363300043738154 -0.163015745735530 0.000000000000000 +0.828947368421053 -0.430980932247516 -0.231464655405828 0.000000000000000 +0.842105263157895 -0.496355153812509 -0.306968945910482 0.000000000000000 +0.855263157894737 -0.559145439776727 -0.387431528960906 0.000000000000000 +0.868421052631579 -0.619074521483765 -0.470755316268511 0.000000000000000 +0.881578947368421 -0.675865130277217 -0.554843219544706 0.000000000000000 +0.894736842105263 -0.729239997500677 -0.637598150500906 0.000000000000000 +0.907894736842105 -0.778921854497740 -0.716923020848520 0.000000000000000 +0.921052631578947 -0.824633432612001 -0.790720742298961 0.000000000000000 +0.934210526315789 -0.866097463187054 -0.856894226563639 0.000000000000000 +0.947368421052632 -0.903036677566493 -0.913346385353967 0.000000000000000 +0.960526315789474 -0.935173807093912 -0.957980130381355 0.000000000000000 +0.973684210526316 -0.962231583112907 -0.988698373357216 0.000000000000000 +0.986842105263158 -0.983932736967071 -1.003404025992960 0.000000000000000 +1.000000000000000 -1.000000000000000 -1.000000000000000 0.000000000000000 diff --git a/test/test_visualization.jl b/test/test_visualization.jl new file mode 100644 index 00000000..66bd6fa7 --- /dev/null +++ b/test/test_visualization.jl @@ -0,0 +1,155 @@ +module TestVisualization +#= + Visualization tests for "Viz/VizProject.jl" functions and "Meshing.jl" + +Functions: @ = tested + + @ plotProject! + @ updatePlot! + @ generate_mesh(p::project) + @ remove_mesh!(p::project) +=# +using HOHQMesh +using Test + +# We use CairoMakie to avoid some CI-related issues with GLMakie. CairoMakie can be used +# as a testing backend for interactive mesh tool's Makie-based visualization. +using CairoMakie + +@testset "Visualization Tests" begin + + projectName = "CirclesInCircle" + projectPath = "out" + + p_visu = newProject(projectName, projectPath) + # Outer boundary + circ = new("outerCircle", [0.0, -1.0, 0.0], 4.0, 0.0, 360.0, "degrees") + add!(p_visu, circ) + + # Test getting the outer curve name + dict = getCurve(p_visu, "outerCircle") + @test dict["TYPE"] == "CIRCULAR_ARC" + + # First inner boundary via a spline from a file + spline1 = new("big_spline", joinpath(@__DIR__, "test_spline_curve_data.txt")) + add!(p_visu, spline1, "inner1") + + # Test extracting an inner boundary chain with the generic function + tup = getInnerBoundary(p_visu, "inner1") + @test tup[2]["TYPE"] == "CHAIN" + + # Attempt to generate the mesh before the background grid is set. Throws a warning. + @test_logs (:warn, "A background grid is needed before meshing. Add one and try again.") generate_mesh(p_visu) + + # There is no background grid. Query the different styles of background grids + # to test that errors are thrown correctly. + @test_throws ErrorException getBackgroundGridSize(p_visu) + @test_throws ErrorException getBackgroundGridLowerLeft(p_visu) + @test_throws ErrorException getBackgroundGridSteps(p_visu) + + # To mesh, a background grid is needed + addBackgroundGrid!(p_visu, [0.6, 0.6, 0.0]) + + # Set file format to ISM-V2 and corresponding output file names + setMeshFileFormat!(p_visu, "ISM-V2") + meshFileFormat = getMeshFileFormat(p_visu) + setFileNames!(p_visu, meshFileFormat) + + # Show initial the model and grid + @test_nowarn plotProject!(p_visu, MODEL+GRID) + + # Create the mesh which contains a plotting update for ISM + @test_nowarn generate_mesh(p_visu) + + # Destroy the mesh and reset the background grid + @test_nowarn remove_mesh!(p_visu) + + # Add another inner boundary via a spline with given data points + data = [ [0.0 1.75 -1.0 0.0] + [0.25 2.1 -0.5 0.0] + [0.5 2.7 -1.0 0.0] + [0.75 0.6 -2.0 0.0] + [1.0 1.75 -1.0 0.0] ] + spline2 = new("small_spline", 5, data) + add!(p_visu, spline2, "inner2") + + # + # Test getting the inner curve + # + # Purposely get the names wrong to throw a warning + @test_logs (:warn, "No curve small_spline in boundary inner1. Try again.") dict = getCurve(p_visu, "small_spline", "inner1") + # Do it correctly this time + dict = getCurve(p_visu, "small_spline", "inner2") + @test dict["TYPE"] == "SPLINE_CURVE" + + # Set file format to ISM (to exericise plotting routine) + setMeshFileFormat!(p_visu, "ISM") + meshFileFormat = getMeshFileFormat(p_visu) + setFileNames!(p_visu, meshFileFormat) + + @test_nowarn updatePlot!(p_visu) + + # Create the mesh which contains a plotting update for ISM-V2 + @test_nowarn generate_mesh(p_visu) + + # Destroy the mesh and reset the background grid + @test_nowarn remove_mesh!(p_visu) + + # Add a final inner boundary that contains multiple links in the chain + edge1 = newEndPointsLineCurve("edge1", [-2.3, -1.0, 0.0], [-1.7, -1.0, 0.0]) + edge2 = newEndPointsLineCurve("edge2", [-1.7, -1.0, 0.0], [-2.0, -0.4, 0.0]) + edge3 = newEndPointsLineCurve("edge3", [-2.0, -0.4, 0.0], [-2.3, -1.0, 0.0]) + add!(p_visu, edge1, "inner3") + add!(p_visu, edge2, "inner3") + add!(p_visu, edge3, "inner3") + + # Create a refinement center and add it with the generic method + cent = newRefinementCenter("Center1", "smooth", [-1.25, -3.0, 0.0], 0.2, 1.0) + add!(p_visu, cent) + + # Set file format to ABAQUS (to exericise plotting routine) + setMeshFileFormat!(p_visu, "ABAQUS") + meshFileFormat = getMeshFileFormat(p_visu) + setFileNames!(p_visu, meshFileFormat) + + @test_nowarn updatePlot!(p_visu, MODEL+GRID+REFINEMENTS) + + # Create the mesh which contains a plotting update for ABAQUS + @test_nowarn generate_mesh(p_visu) + + # Remove the outer boundary from the project + remove!(p_visu, "outerCircle") + + # + # Remove the inner boundaries from the project + # + + # Purposely do this wrong to throw a warning + # (1) Give a wrong "new" inner boundary name + @test_throws ErrorException remove!(p_visu, "big_spline", "wrongName") + # (2) Give the wrong inner boundary name that exists but does not contain "big_spline" + @test_throws ErrorException remove!(p_visu, "big_spline", "inner2") + # (3) Give the correct combination and remove the inner boundary + remove!(p_visu, "big_spline", "inner1") + @test length(p_visu.innerBoundaryNames) == 2 + + # Do the rest of the inner boudary removals correctly. + remove!(p_visu, "small_spline", "inner2") + @test length(p_visu.innerBoundaryNames) == 1 + undo() + @test length(p_visu.innerBoundaryNames) == 2 + redo() + + # Remove a single part of the chain with multiple curves + @test length(p_visu.innerBoundaryNames[1]) == 3 + remove!(p_visu, "edge2", "inner3") + @test length(p_visu.innerBoundaryNames[1]) == 2 + undo() + # To remove the inner boundary with multiple chains we use a different method. + removeInnerBoundary!(p_visu, "inner3") + undo() + redo() + +end + +end #module \ No newline at end of file